feat(web): initial bilingual landing page and interactive web cockpit
This commit is contained in:
commit
f0a241fae0
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
benzcloud-server
|
||||
benzcloud-server.exe
|
||||
android/
|
||||
data/
|
||||
17
404.html
Normal file
17
404.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Seite nicht gefunden · BenzCloud Server</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body style="display: flex; justify-content: center; align-items: center; text-align: center; height: 100vh;">
|
||||
<div style="max-width: 500px; padding: 2rem;">
|
||||
<h1 style="font-size: 4rem; color: var(--accent-primary); margin-bottom: 1rem;">404</h1>
|
||||
<h2 style="margin-bottom: 1rem;">Seite nicht gefunden / Page Not Found</h2>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 2rem;">Die angeforderte Ressource existiert nicht auf diesem Server.</p>
|
||||
<a href="index.html" class="btn-primary" style="display: inline-block;">Zur Startseite</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
644
app/app.js
Normal file
644
app/app.js
Normal file
@ -0,0 +1,644 @@
|
||||
// BenzCloud Frontend Application Engine
|
||||
// Author: Jeremy Benz (@benzjeremy) • GNU GPL-3.0
|
||||
|
||||
let currentLang = localStorage.getItem("benzcloud_lang") || "de";
|
||||
let sessionToken = localStorage.getItem("benzcloud_token") || "";
|
||||
let systemConfig = null;
|
||||
let currentPath = "";
|
||||
|
||||
const i18n = {
|
||||
de: {
|
||||
setup_badge: "✨ One-Click Ersteinrichtung",
|
||||
setup_title: "Willkommen bei BenzCloud",
|
||||
setup_desc: "Deine datenschutzfreundliche, lokale Enterprise-Suite. Trage deine Wunsch-Domain ein – Mesh-VPN, DNS-Server und Routing konfigurieren sich vollautomatisch ohne Router-Portfreigaben.",
|
||||
lbl_domain: "Wunsch-Domain (Basis für System & Dienste):",
|
||||
hint_domain: "Subdomains wie vpn, drive und mail werden automatisch erzeugt.",
|
||||
lbl_admin_user: "Administrator-Benutzername:",
|
||||
lbl_admin_pass: "Administrator-Passwort (mind. 8 Zeichen):",
|
||||
sec_title: "Zero-Dummy-Security ab Werk:",
|
||||
sec_desc: "PBKDF2 Schlüsselableitung (100.000 Runden), AES-256-GCM Verschlüsselung at Rest und gegenseitig authentifiziertes Slack Nebula P2P-Mesh.",
|
||||
btn_start_setup: "🚀 BenzCloud jetzt automatisch einrichten",
|
||||
login_title: "BenzCloud Anmeldung",
|
||||
login_desc: "Melde dich mit deinem Konto an, um auf das Cockpit und deine Dienste zuzugreifen.",
|
||||
lbl_username: "Benutzername:",
|
||||
lbl_password: "Passwort:",
|
||||
btn_login: "Anmelden",
|
||||
logout: "Abmelden",
|
||||
m_domain: "Basis-Domain",
|
||||
m_vpn: "Mesh-VPN (Nebula)",
|
||||
m_dns: "Custom DNS Server",
|
||||
m_peers: "Verbundene Peers",
|
||||
m_peers_sub: "P2P Mesh Nodes",
|
||||
tab_overview: "🧭 Schnellstart",
|
||||
tab_drive: "📁 Drive (Cloud-Dateien)",
|
||||
tab_vpn: "🛡️ Mesh-VPN & Peers",
|
||||
tab_dns: "🌐 DNS & Routing",
|
||||
tab_plugins: "🧩 Plugins",
|
||||
tab_users: "👥 Benutzerverwaltung",
|
||||
services_title: "Verfügbare Enterprise-Dienste",
|
||||
srv_drive: "BenzCloud Drive",
|
||||
srv_drive_desc: "Verschlüsselter Dateispeicher, Dokumentenablage & Dateifreigabe (Nextcloud-Alternative).",
|
||||
srv_mail: "BenzCloud Mail",
|
||||
srv_mail_desc: "Geschlossenes internes E-Mail-System (SMTP/IMAP) mit integrierter Webmail-Oberfläche.",
|
||||
srv_chat: "BenzCloud Chat",
|
||||
srv_chat_desc: "Echtzeit-Team-Chat (Slack/Teams-Alternative) für verschlüsselte Kommunikation.",
|
||||
srv_web: "Web-Hosting Engine",
|
||||
srv_web_desc: "Eigene Webseiten unter frei wählbaren Subdomains (HTML, PHP, Astro).",
|
||||
pair_title: "Client-Kopplung (Android & PC)",
|
||||
pair_desc: "Verbinde die Client-App einmalig über die lokale Server-IP. Danach läuft die gesamte Kommunikation automatisch verschlüsselt über das Mesh-VPN und den Custom-DNS.",
|
||||
pair_server_endpoint: "Server LAN-Kopplungs-Endpunkt:",
|
||||
pair_cli_cmd: "Verbindungsbefehl (BenzCloud-Client CLI):",
|
||||
drive_vault_title: "Verschlüsselter Dateispeicher (AES-256-GCM)",
|
||||
btn_upload: "⬆️ Datei hochladen",
|
||||
btn_new_folder: "📁 Neuer Ordner",
|
||||
th_name: "Name",
|
||||
th_size: "Größe",
|
||||
th_date: "Änderungsdatum",
|
||||
th_actions: "Aktionen",
|
||||
loading_files: "Lade Dateien...",
|
||||
vpn_title: "Slack Nebula Mesh-VPN & Peer-Topologie",
|
||||
vpn_desc: "Das P2P Mesh-Overlay verbindet alle autorisierten Endgeräte verschlüsselt ohne Portweiterleitungen am Router.",
|
||||
th_peer_name: "Node / Peer Name",
|
||||
th_overlay_ip: "Overlay IP",
|
||||
th_role: "Rolle",
|
||||
th_status: "Status",
|
||||
th_valid_until: "Zertifikat gültig bis",
|
||||
dns_title: "Custom DNS Server & Subdomain-Routing",
|
||||
dns_records_title: "Autoritative System-Subdomains",
|
||||
dns_logs_title: "Live DNS-Abfragen (RFC 1035)",
|
||||
plugins_title: "Modulare Enterprise-Plugins",
|
||||
plugins_desc: "Der Server orchestriert Module in getrennten Prozessen über token-gesicherte Schnittstellen.",
|
||||
users_title: "Benutzer- und Rechteverwaltung",
|
||||
btn_create_user: "➕ Neuer Benutzer",
|
||||
th_user: "Benutzer",
|
||||
th_perms: "Berechtigungen",
|
||||
th_base_perms: "Basis-Privilegien (Fixiert)",
|
||||
modal_create_user_title: "Neuen Benutzer anlegen",
|
||||
immutable_perms_text: "VPN & DNS sind unveränderlich aktiv und können nicht entzogen werden.",
|
||||
btn_cancel: "Abbrechen",
|
||||
btn_save: "Erstellen",
|
||||
pre_release_pill: "Pre-Release / In aktiver Entwicklung",
|
||||
btn_toggle_on: "Aktiviert",
|
||||
btn_toggle_off: "Deaktiviert",
|
||||
btn_download: "⬇️ Herunterladen",
|
||||
btn_delete: "🗑️ Löschen",
|
||||
prompt_folder_name: "Name des neuen Ordners:",
|
||||
confirm_delete: "Möchtest du diese Datei wirklich löschen?"
|
||||
},
|
||||
en: {
|
||||
setup_badge: "✨ One-Click Initial Setup",
|
||||
setup_title: "Welcome to BenzCloud",
|
||||
setup_desc: "Your privacy-first, local enterprise suite. Enter your desired domain – Mesh-VPN, DNS server, and routing configure automatically without router port forwards.",
|
||||
lbl_domain: "Target Domain (Base for system & services):",
|
||||
hint_domain: "Subdomains like vpn, drive, and mail will be generated automatically.",
|
||||
lbl_admin_user: "Administrator Username:",
|
||||
lbl_admin_pass: "Administrator Password (min. 8 characters):",
|
||||
sec_title: "Zero-Dummy-Security by Design:",
|
||||
sec_desc: "PBKDF2 key derivation (100,000 rounds), AES-256-GCM encryption at rest, and mutually authenticated Slack Nebula P2P mesh.",
|
||||
btn_start_setup: "🚀 Automatically configure BenzCloud now",
|
||||
login_title: "BenzCloud Authentication",
|
||||
login_desc: "Sign in with your account to access your cockpit and services.",
|
||||
lbl_username: "Username:",
|
||||
lbl_password: "Password:",
|
||||
btn_login: "Sign In",
|
||||
logout: "Log Out",
|
||||
m_domain: "Base Domain",
|
||||
m_vpn: "Mesh-VPN (Nebula)",
|
||||
m_dns: "Custom DNS Server",
|
||||
m_peers: "Connected Peers",
|
||||
m_peers_sub: "P2P Mesh Nodes",
|
||||
tab_overview: "🧭 Quickstart",
|
||||
tab_drive: "📁 Drive (Cloud Files)",
|
||||
tab_vpn: "🛡️ Mesh-VPN & Peers",
|
||||
tab_dns: "🌐 DNS & Routing",
|
||||
tab_plugins: "🧩 Plugins",
|
||||
tab_users: "👥 User Management",
|
||||
services_title: "Available Enterprise Services",
|
||||
srv_drive: "BenzCloud Drive",
|
||||
srv_drive_desc: "Encrypted file vault, document store & file sharing (Nextcloud alternative).",
|
||||
srv_mail: "BenzCloud Mail",
|
||||
srv_mail_desc: "Closed internal email system (SMTP/IMAP) with integrated webmail client.",
|
||||
srv_chat: "BenzCloud Chat",
|
||||
srv_chat_desc: "Real-time team chat (Slack/Teams alternative) for direct communication.",
|
||||
srv_web: "Web-Hosting Engine",
|
||||
srv_web_desc: "Deploy custom websites under arbitrary subdomains (HTML, PHP, Astro).",
|
||||
pair_title: "Client Pairing (Android & PC)",
|
||||
pair_desc: "Connect your client app once via local server IP. Afterward, all traffic automatically flows securely through Mesh-VPN and custom DNS.",
|
||||
pair_server_endpoint: "Server LAN Pairing Endpoint:",
|
||||
pair_cli_cmd: "Connect Command (BenzCloud-Client CLI):",
|
||||
drive_vault_title: "Encrypted File Storage (AES-256-GCM)",
|
||||
btn_upload: "⬆️ Upload File",
|
||||
btn_new_folder: "📁 New Folder",
|
||||
th_name: "Name",
|
||||
th_size: "Size",
|
||||
th_date: "Modified Date",
|
||||
th_actions: "Actions",
|
||||
loading_files: "Loading files...",
|
||||
vpn_title: "Slack Nebula Mesh-VPN & Peer Topology",
|
||||
vpn_desc: "The P2P mesh overlay connects all authorized endpoints securely without router port forwarding.",
|
||||
th_peer_name: "Node / Peer Name",
|
||||
th_overlay_ip: "Overlay IP",
|
||||
th_role: "Role",
|
||||
th_status: "Status",
|
||||
th_valid_until: "Certificate Valid Until",
|
||||
dns_title: "Custom DNS Server & Subdomain Routing",
|
||||
dns_records_title: "Authoritative System Subdomains",
|
||||
dns_logs_title: "Live DNS Queries (RFC 1035)",
|
||||
plugins_title: "Modular Enterprise Plugins",
|
||||
plugins_desc: "The server orchestrates modules in separate processes across token-authenticated interfaces.",
|
||||
users_title: "User & Rights Management",
|
||||
btn_create_user: "➕ New User",
|
||||
th_user: "User",
|
||||
th_perms: "Permissions",
|
||||
th_base_perms: "Base Privileges (Locked)",
|
||||
modal_create_user_title: "Create New User",
|
||||
immutable_perms_text: "VPN & DNS are permanently active and cannot be revoked.",
|
||||
btn_cancel: "Cancel",
|
||||
btn_save: "Create",
|
||||
pre_release_pill: "Pre-Release / In Active Development",
|
||||
btn_toggle_on: "Enabled",
|
||||
btn_toggle_off: "Disabled",
|
||||
btn_download: "⬇️ Download",
|
||||
btn_delete: "🗑️ Delete",
|
||||
prompt_folder_name: "Name of new folder:",
|
||||
confirm_delete: "Are you sure you want to delete this file?"
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
applyLanguage(currentLang);
|
||||
checkSystemStatus();
|
||||
setInterval(refreshActiveTabData, 3000);
|
||||
});
|
||||
|
||||
function setLanguage(lang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem("benzcloud_lang", lang);
|
||||
applyLanguage(lang);
|
||||
}
|
||||
|
||||
function applyLanguage(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
document.querySelectorAll("[data-i18n]").forEach(el => {
|
||||
const key = el.getAttribute("data-i18n");
|
||||
if (i18n[lang] && i18n[lang][key]) {
|
||||
el.textContent = i18n[lang][key];
|
||||
}
|
||||
});
|
||||
document.getElementById("langDE").classList.toggle("active", lang === "de");
|
||||
document.getElementById("langEN").classList.toggle("active", lang === "en");
|
||||
}
|
||||
|
||||
async function checkSystemStatus() {
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
const data = await res.json();
|
||||
systemConfig = data;
|
||||
|
||||
document.getElementById("headerDomainPill").textContent = data.base_domain || "intern";
|
||||
document.getElementById("mDomain").textContent = data.base_domain || "-";
|
||||
document.getElementById("mLocalIP").textContent = "LAN: " + (data.server_local_ip || "127.0.0.1");
|
||||
document.getElementById("mVPNIP").textContent = "Overlay IP: " + (data.server_vpn_ip || "10.42.0.1");
|
||||
document.getElementById("mDNSPort").textContent = `Port ${data.dns_port} (UDP & TCP)`;
|
||||
document.getElementById("mPeers").textContent = data.mesh_peers || "0";
|
||||
|
||||
// Update pairing links
|
||||
const pairUrl = `http://${data.server_local_ip}:${data.http_port}/api/pair`;
|
||||
document.getElementById("pairEndpoint").textContent = pairUrl;
|
||||
document.getElementById("pairCliCmd").textContent = `benzcloud-client pair -server http://${data.server_local_ip}:${data.http_port} -user admin`;
|
||||
|
||||
// Service URLs
|
||||
document.getElementById("urlDrive").textContent = `http://drive.${data.base_domain}`;
|
||||
document.getElementById("linkDrive").href = `http://drive.${data.base_domain}`;
|
||||
document.getElementById("urlMail").textContent = `http://mail.${data.base_domain}`;
|
||||
document.getElementById("linkMail").href = `http://mail.${data.base_domain}`;
|
||||
document.getElementById("urlChat").textContent = `http://chat.${data.base_domain}`;
|
||||
document.getElementById("linkChat").href = `http://chat.${data.base_domain}`;
|
||||
document.getElementById("urlWeb").textContent = `http://*.${data.base_domain}`;
|
||||
document.getElementById("linkWeb").href = `http://${data.base_domain}`;
|
||||
|
||||
if (!data.setup_completed) {
|
||||
showView("setupWizard");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionToken) {
|
||||
showView("loginView");
|
||||
return;
|
||||
}
|
||||
|
||||
showView("dashboardView");
|
||||
document.getElementById("btnLogout").style.display = "block";
|
||||
loadDashboardData();
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch system status:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function showView(viewId) {
|
||||
document.getElementById("setupWizard").style.display = viewId === "setupWizard" ? "block" : "none";
|
||||
document.getElementById("loginView").style.display = viewId === "loginView" ? "block" : "none";
|
||||
document.getElementById("dashboardView").style.display = viewId === "dashboardView" ? "block" : "none";
|
||||
}
|
||||
|
||||
async function submitSetup(e) {
|
||||
e.preventDefault();
|
||||
const baseDomain = document.getElementById("setupDomain").value.trim();
|
||||
const adminUser = document.getElementById("setupAdminUser").value.trim();
|
||||
const adminPass = document.getElementById("setupAdminPass").value;
|
||||
|
||||
const btn = document.getElementById("btnSubmitSetup");
|
||||
btn.disabled = true;
|
||||
btn.textContent = currentLang === "de" ? "⚙️ Initialisiere Mesh-VPN, DNS und Zertifikate..." : "⚙️ Initializing Mesh-VPN, DNS and certificates...";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/setup", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
base_domain: baseDomain,
|
||||
admin_username: adminUser,
|
||||
admin_password: adminPass
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert("Fehler: " + (data.error || "Setup fehlgeschlagen"));
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_start_setup;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionToken = data.session_token;
|
||||
localStorage.setItem("benzcloud_token", sessionToken);
|
||||
checkSystemStatus();
|
||||
} catch (err) {
|
||||
alert("Netzwerkfehler: " + err);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitLogin(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById("loginUser").value.trim();
|
||||
const password = document.getElementById("loginPass").value;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(currentLang === "de" ? "Ungültige Anmeldedaten" : "Invalid credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
sessionToken = data.token;
|
||||
localStorage.setItem("benzcloud_token", sessionToken);
|
||||
checkSystemStatus();
|
||||
} catch (err) {
|
||||
alert("Netzwerkfehler: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
sessionToken = "";
|
||||
localStorage.removeItem("benzcloud_token");
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function switchTab(tabId) {
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => btn.classList.remove("active"));
|
||||
document.querySelectorAll(".tab-pane").forEach(pane => pane.classList.remove("active"));
|
||||
|
||||
event.target.classList.add("active");
|
||||
const targetPane = document.getElementById(`tabContent_${tabId}`);
|
||||
if (targetPane) targetPane.classList.add("active");
|
||||
|
||||
refreshActiveTabData();
|
||||
}
|
||||
|
||||
function refreshActiveTabData() {
|
||||
if (!sessionToken || !systemConfig || !systemConfig.setup_completed) return;
|
||||
const activePane = document.querySelector(".tab-pane.active");
|
||||
if (!activePane) return;
|
||||
|
||||
const id = activePane.id;
|
||||
if (id === "tabContent_drive") loadDriveFiles();
|
||||
if (id === "tabContent_vpn") loadVPNPeers();
|
||||
if (id === "tabContent_dns") loadDNSData();
|
||||
if (id === "tabContent_plugins") loadPlugins();
|
||||
if (id === "tabContent_users") loadUsers();
|
||||
}
|
||||
|
||||
function loadDashboardData() {
|
||||
loadDriveFiles();
|
||||
loadVPNPeers();
|
||||
loadDNSData();
|
||||
loadPlugins();
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
/* DRIVE */
|
||||
async function loadDriveFiles() {
|
||||
try {
|
||||
const res = await fetch(`/api/drive/files?path=${encodeURIComponent(currentPath)}`, {
|
||||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("fileTableBody");
|
||||
tbody.innerHTML = "";
|
||||
|
||||
if (!data.files || data.files.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted">${currentLang === "de" ? "Keine Dateien vorhanden." : "No files available."}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
data.files.forEach(f => {
|
||||
const tr = document.createElement("tr");
|
||||
const icon = f.is_dir ? "📁" : "📄";
|
||||
const sizeStr = f.is_dir ? "-" : formatBytes(f.size);
|
||||
const dateStr = new Date(f.mod_time).toLocaleString();
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${icon} <strong>${escapeHtml(f.name)}</strong></td>
|
||||
<td>${sizeStr}</td>
|
||||
<td>${dateStr}</td>
|
||||
<td>
|
||||
${!f.is_dir ? `<a class="btn-sm btn-primary" href="/api/drive/download?path=${encodeURIComponent(f.path)}&token=${sessionToken}">${i18n[currentLang].btn_download}</a>` : ""}
|
||||
<button class="btn-sm btn-outline" onclick="deleteFile('${escapeHtml(f.path)}')">${i18n[currentLang].btn_delete}</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Drive load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadSelectedFile() {
|
||||
const input = document.getElementById("fileUploadInput");
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
const file = input.files[0];
|
||||
const targetPath = currentPath ? `${currentPath}/${file.name}` : file.name;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/drive/upload?path=${encodeURIComponent(targetPath)}`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${sessionToken}` },
|
||||
body: file
|
||||
});
|
||||
if (res.ok) {
|
||||
loadDriveFiles();
|
||||
} else {
|
||||
alert("Upload failed");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Upload error: " + err);
|
||||
}
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
async function promptNewFolder() {
|
||||
const name = prompt(i18n[currentLang].prompt_folder_name);
|
||||
if (!name) return;
|
||||
const folderPath = currentPath ? `${currentPath}/${name}` : name;
|
||||
try {
|
||||
await fetch("/api/drive/folder", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${sessionToken}`
|
||||
},
|
||||
body: JSON.stringify({ path: folderPath })
|
||||
});
|
||||
loadDriveFiles();
|
||||
} catch (err) {
|
||||
alert("Folder creation failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile(path) {
|
||||
if (!confirm(i18n[currentLang].confirm_delete)) return;
|
||||
try {
|
||||
await fetch(`/api/drive/delete?path=${encodeURIComponent(path)}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
});
|
||||
loadDriveFiles();
|
||||
} catch (err) {
|
||||
alert("Delete failed");
|
||||
}
|
||||
}
|
||||
|
||||
/* VPN PEERS */
|
||||
async function loadVPNPeers() {
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("peerTableBody");
|
||||
tbody.innerHTML = "";
|
||||
|
||||
// Server Node
|
||||
const sTr = document.createElement("tr");
|
||||
sTr.innerHTML = `
|
||||
<td><strong>${escapeHtml(data.system)} (Lighthouse)</strong></td>
|
||||
<td><code>${data.server_vpn_ip}</code></td>
|
||||
<td><span class="badge-tag badge-blue">Lighthouse</span></td>
|
||||
<td><span class="pulse-dot"></span> Online</td>
|
||||
<td>Permanenter Host</td>
|
||||
`;
|
||||
tbody.appendChild(sTr);
|
||||
} catch (err) {
|
||||
console.error("VPN peers load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/* DNS & ROUTING */
|
||||
async function loadDNSData() {
|
||||
if (!systemConfig) return;
|
||||
const domain = systemConfig.base_domain;
|
||||
const ip = systemConfig.server_vpn_ip;
|
||||
|
||||
const recordsList = document.getElementById("dnsRecordsList");
|
||||
recordsList.innerHTML = `
|
||||
<div class="record-row"><span>vpn.${domain}</span><code>${ip}</code></div>
|
||||
<div class="record-row"><span>drive.${domain}</span><code>${ip}</code></div>
|
||||
<div class="record-row"><span>mail.${domain}</span><code>${ip}</code></div>
|
||||
<div class="record-row"><span>chat.${domain}</span><code>${ip}</code></div>
|
||||
<div class="record-row"><span>*.${domain} (Web Engine)</span><code>${ip}</code></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/dns/logs", {
|
||||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const logsList = document.getElementById("dnsLogsList");
|
||||
logsList.innerHTML = "";
|
||||
|
||||
if (!data.logs || data.logs.length === 0) {
|
||||
logsList.innerHTML = `<div class="p-item text-muted">${currentLang === "de" ? "Noch keine externen DNS-Anfragen eingetroffen." : "No DNS queries logged yet."}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
data.logs.slice(-8).reverse().forEach(log => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "log-row";
|
||||
row.innerHTML = `
|
||||
<span><strong>${escapeHtml(log.domain)}</strong> (${escapeHtml(log.type)})</span>
|
||||
<code>${escapeHtml(log.answer)}</code>
|
||||
`;
|
||||
logsList.appendChild(row);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("DNS logs load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/* PLUGINS */
|
||||
async function loadPlugins() {
|
||||
try {
|
||||
const res = await fetch("/api/plugins", {
|
||||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const grid = document.getElementById("pluginCardsGrid");
|
||||
grid.innerHTML = "";
|
||||
|
||||
data.plugins.forEach(p => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "plugin-card";
|
||||
card.innerHTML = `
|
||||
<div class="p-header">
|
||||
<div class="p-title">${escapeHtml(p.name)}</div>
|
||||
<span class="badge-tag ${p.enabled ? 'badge-green' : 'badge-tag'}">${p.enabled ? i18n[currentLang].btn_toggle_on : i18n[currentLang].btn_toggle_off}</span>
|
||||
</div>
|
||||
<p class="text-muted" style="font-size:0.85rem;">Binary: <code>${escapeHtml(p.binary)}</code> | Port: <code>${p.port}</code></p>
|
||||
<p class="text-muted" style="font-size:0.85rem;">Subdomains: <code>${p.subdomains.length ? p.subdomains.join(', ') : 'Frei wählbar'}</code></p>
|
||||
<div style="margin-top:auto;">
|
||||
<button class="btn-sm ${p.enabled ? 'btn-outline' : 'btn-primary'}" onclick="togglePlugin('${p.id}', ${!p.enabled})">
|
||||
${p.enabled ? (currentLang === 'de' ? 'Deaktivieren' : 'Disable') : (currentLang === 'de' ? 'Aktivieren' : 'Enable')}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Plugins load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePlugin(id, enabled) {
|
||||
try {
|
||||
await fetch("/api/plugins/toggle", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${sessionToken}`
|
||||
},
|
||||
body: JSON.stringify({ id, enabled })
|
||||
});
|
||||
loadPlugins();
|
||||
} catch (err) {
|
||||
alert("Plugin toggle failed");
|
||||
}
|
||||
}
|
||||
|
||||
/* USERS */
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch("/api/users", {
|
||||
headers: { Authorization: `Bearer ${sessionToken}` }
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("usersTableBody");
|
||||
tbody.innerHTML = "";
|
||||
|
||||
data.users.forEach(u => {
|
||||
const tr = document.createElement("tr");
|
||||
let permsBadges = [];
|
||||
if (u.permissions.admin) permsBadges.push('<span class="badge-tag badge-blue">Admin</span>');
|
||||
if (u.permissions.drive) permsBadges.push('<span class="badge-tag badge-green">Drive</span>');
|
||||
if (u.permissions.mail) permsBadges.push('<span class="badge-tag badge-green">Mail</span>');
|
||||
if (u.permissions.chat) permsBadges.push('<span class="badge-tag badge-green">Chat</span>');
|
||||
if (u.permissions.web) permsBadges.push('<span class="badge-tag badge-green">Web</span>');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td><strong>${escapeHtml(u.username)}</strong></td>
|
||||
<td><code>${u.overlay_ip}</code></td>
|
||||
<td>${permsBadges.join(' ')}</td>
|
||||
<td><span class="badge-tag badge-locked">🔒 VPN & DNS (Immer aktiv)</span></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Users load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateUserModal() {
|
||||
document.getElementById("createUserModal").style.display = "flex";
|
||||
}
|
||||
|
||||
function closeCreateUserModal() {
|
||||
document.getElementById("createUserModal").style.display = "none";
|
||||
}
|
||||
|
||||
async function submitCreateUser(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById("newUsername").value.trim();
|
||||
const password = document.getElementById("newPassword").value;
|
||||
const drive = document.getElementById("permDrive").checked;
|
||||
const mail = document.getElementById("permMail").checked;
|
||||
const chat = document.getElementById("permChat").checked;
|
||||
const web = document.getElementById("permWeb").checked;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${sessionToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
permissions: { admin: false, drive, mail, chat, web, vpn: true, dns: true }
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
alert("Fehler beim Erstellen des Benutzers");
|
||||
return;
|
||||
}
|
||||
closeCreateUserModal();
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
alert("Netzwerkfehler: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return "";
|
||||
return String(str).replace(/[&<>"']/g, m => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
}[m]));
|
||||
}
|
||||
317
app/index.html
Normal file
317
app/index.html
Normal file
@ -0,0 +1,317 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BenzCloud – Micro-Enterprise Suite</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<div class="logo-icon">☁️</div>
|
||||
<div class="logo-text">
|
||||
<span class="brand-title">BenzCloud</span>
|
||||
<span class="brand-badge">v1.0</span>
|
||||
</div>
|
||||
<span class="domain-pill" id="headerDomainPill">intern</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="lang-switcher">
|
||||
<button id="langDE" class="btn-lang active" onclick="setLanguage('de')">DE</button>
|
||||
<button id="langEN" class="btn-lang" onclick="setLanguage('en')">EN</button>
|
||||
</div>
|
||||
<button class="btn-sm btn-outline" id="btnLogout" onclick="logout()" style="display:none;" data-i18n="logout">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- SETUP WIZARD (Visible when system not yet configured) -->
|
||||
<div id="setupWizard" class="view-panel" style="display: none;">
|
||||
<div class="wizard-card">
|
||||
<div class="wizard-badge" data-i18n="setup_badge">✨ One-Click Ersteinrichtung</div>
|
||||
<h1 class="wizard-title" data-i18n="setup_title">Willkommen bei BenzCloud</h1>
|
||||
<p class="wizard-desc" data-i18n="setup_desc">Deine datenschutzfreundliche, lokale Enterprise-Suite. Trage deine Wunsch-Domain ein – Mesh-VPN, DNS-Server und Routing konfigurieren sich vollautomatisch ohne Router-Portfreigaben.</p>
|
||||
|
||||
<form id="setupForm" onsubmit="submitSetup(event)" class="wizard-form">
|
||||
<div class="form-group">
|
||||
<label for="setupDomain" data-i18n="lbl_domain">Wunsch-Domain (Basis für System & Dienste):</label>
|
||||
<div class="input-with-hint">
|
||||
<input type="text" id="setupDomain" required placeholder="z. B. benzjeremy.de oder intern" value="benzjeremy.de">
|
||||
<small class="form-hint" data-i18n="hint_domain">Subdomains wie vpn, drive und mail werden automatisch erzeugt.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="setupAdminUser" data-i18n="lbl_admin_user">Administrator-Benutzername:</label>
|
||||
<input type="text" id="setupAdminUser" required placeholder="admin" value="admin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupAdminPass" data-i18n="lbl_admin_pass">Administrator-Passwort (mind. 8 Zeichen):</label>
|
||||
<input type="password" id="setupAdminPass" required placeholder="••••••••" minlength="8">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-security-notice">
|
||||
<span class="sec-icon">🔒</span>
|
||||
<div>
|
||||
<strong data-i18n="sec_title">Zero-Dummy-Security ab Werk:</strong>
|
||||
<p data-i18n="sec_desc">PBKDF2 Schlüsselableitung (100.000 Runden), AES-256-GCM Verschlüsselung at Rest und gegenseitig authentifiziertes Slack Nebula P2P-Mesh.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-lg" id="btnSubmitSetup" data-i18n="btn_start_setup">
|
||||
🚀 BenzCloud jetzt automatisch einrichten
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LOGIN MODAL / VIEW (When setup is done but not authenticated) -->
|
||||
<div id="loginView" class="view-panel" style="display: none;">
|
||||
<div class="auth-card">
|
||||
<h2 data-i18n="login_title">BenzCloud Anmeldung</h2>
|
||||
<p class="text-muted" data-i18n="login_desc">Melde dich mit deinem Konto an, um auf das Cockpit und deine Dienste zuzugreifen.</p>
|
||||
<form id="loginForm" onsubmit="submitLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="loginUser" data-i18n="lbl_username">Benutzername:</label>
|
||||
<input type="text" id="loginUser" required placeholder="admin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPass" data-i18n="lbl_password">Passwort:</label>
|
||||
<input type="password" id="loginPass" required placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-block" data-i18n="btn_login">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN DASHBOARD -->
|
||||
<div id="dashboardView" class="view-panel" style="display: none;">
|
||||
<!-- System Metrics Banner -->
|
||||
<section class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="m-label" data-i18n="m_domain">Basis-Domain</div>
|
||||
<div class="m-value" id="mDomain">-</div>
|
||||
<div class="m-sub" id="mLocalIP">LAN: -</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="m-label" data-i18n="m_vpn">Mesh-VPN (Nebula)</div>
|
||||
<div class="m-value text-green" id="mVPNStatus"><span class="pulse-dot"></span> Aktiv</div>
|
||||
<div class="m-sub" id="mVPNIP">Overlay IP: 10.42.0.1</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="m-label" data-i18n="m_dns">Custom DNS Server</div>
|
||||
<div class="m-value text-blue" id="mDNSStatus">RFC 1035</div>
|
||||
<div class="m-sub" id="mDNSPort">Port 53 (UDP & TCP)</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="m-label" data-i18n="m_peers">Verbundene Peers</div>
|
||||
<div class="m-value" id="mPeers">0</div>
|
||||
<div class="m-sub" data-i18n="m_peers_sub">P2P Mesh Nodes</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<nav class="tab-nav">
|
||||
<button class="tab-btn active" onclick="switchTab('overview')" data-i18n="tab_overview">🧭 Schnellstart</button>
|
||||
<button class="tab-btn" onclick="switchTab('drive')" data-i18n="tab_drive">📁 Drive (Cloud-Dateien)</button>
|
||||
<button class="tab-btn" onclick="switchTab('vpn')" data-i18n="tab_vpn">🛡️ Mesh-VPN & Peers</button>
|
||||
<button class="tab-btn" onclick="switchTab('dns')" data-i18n="tab_dns">🌐 DNS & Routing</button>
|
||||
<button class="tab-btn" onclick="switchTab('plugins')" data-i18n="tab_plugins">🧩 Plugins</button>
|
||||
<button class="tab-btn" id="tabBtnUsers" onclick="switchTab('users')" data-i18n="tab_users">👥 Benutzerverwaltung</button>
|
||||
</nav>
|
||||
|
||||
<!-- TAB CONTENT: OVERVIEW -->
|
||||
<div id="tabContent_overview" class="tab-pane active">
|
||||
<h2 data-i18n="services_title">Verfügbare Enterprise-Dienste</h2>
|
||||
<div class="services-grid">
|
||||
<a class="service-card" id="linkDrive" href="#" target="_blank">
|
||||
<div class="s-icon">📁</div>
|
||||
<div class="s-info">
|
||||
<h3 data-i18n="srv_drive">BenzCloud Drive</h3>
|
||||
<p class="s-url" id="urlDrive">http://drive.domain</p>
|
||||
<p class="s-desc" data-i18n="srv_drive_desc">Verschlüsselter Dateispeicher, Dokumentenablage & Dateifreigabe (Nextcloud-Alternative).</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="service-card" id="linkMail" href="#" target="_blank">
|
||||
<div class="s-icon">📧</div>
|
||||
<div class="s-info">
|
||||
<h3 data-i18n="srv_mail">BenzCloud Mail</h3>
|
||||
<p class="s-url" id="urlMail">http://mail.domain</p>
|
||||
<p class="s-desc" data-i18n="srv_mail_desc">Geschlossenes internes E-Mail-System (SMTP/IMAP) mit integrierter Webmail-Oberfläche.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="service-card" id="linkChat" href="#" target="_blank">
|
||||
<div class="s-icon">💬</div>
|
||||
<div class="s-info">
|
||||
<h3 data-i18n="srv_chat">BenzCloud Chat</h3>
|
||||
<p class="s-url" id="urlChat">http://chat.domain</p>
|
||||
<p class="s-desc" data-i18n="srv_chat_desc">Echtzeit-Team-Chat (Slack/Teams-Alternative) für verschlüsselte Kommunikation.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="service-card" id="linkWeb" href="#" target="_blank">
|
||||
<div class="s-icon">🌐</div>
|
||||
<div class="s-info">
|
||||
<h3 data-i18n="srv_web">Web-Hosting Engine</h3>
|
||||
<p class="s-url" id="urlWeb">http://*.domain</p>
|
||||
<p class="s-desc" data-i18n="srv_web_desc">Eigene Webseiten unter frei wählbaren Subdomains (HTML, PHP, Astro).</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pairing-box">
|
||||
<div class="pairing-header">
|
||||
<span class="pair-icon">📱</span>
|
||||
<div>
|
||||
<h3 data-i18n="pair_title">Client-Kopplung (Android & PC)</h3>
|
||||
<p data-i18n="pair_desc">Verbinde die Client-App einmalig über die lokale Server-IP. Danach läuft die gesamte Kommunikation automatisch verschlüsselt über das Mesh-VPN und den Custom-DNS.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pairing-details">
|
||||
<div class="p-item">
|
||||
<strong data-i18n="pair_server_endpoint">Server LAN-Kopplungs-Endpunkt:</strong>
|
||||
<code id="pairEndpoint">http://192.168.0.5:8080/api/pair</code>
|
||||
</div>
|
||||
<div class="p-item">
|
||||
<strong data-i18n="pair_cli_cmd">Verbindungsbefehl (BenzCloud-Client CLI):</strong>
|
||||
<code id="pairCliCmd">benzcloud-client pair -server http://192.168.0.5:8080 -user admin</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONTENT: DRIVE -->
|
||||
<div id="tabContent_drive" class="tab-pane">
|
||||
<div class="drive-header">
|
||||
<h2 data-i18n="drive_vault_title">Verschlüsselter Dateispeicher (AES-256-GCM)</h2>
|
||||
<div class="drive-actions">
|
||||
<input type="file" id="fileUploadInput" onchange="uploadSelectedFile()" style="display:none;">
|
||||
<button class="btn-primary" onclick="document.getElementById('fileUploadInput').click()" data-i18n="btn_upload">⬆️ Datei hochladen</button>
|
||||
<button class="btn-secondary" onclick="promptNewFolder()" data-i18n="btn_new_folder">📁 Neuer Ordner</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="th_name">Name</th>
|
||||
<th data-i18n="th_size">Größe</th>
|
||||
<th data-i18n="th_date">Änderungsdatum</th>
|
||||
<th data-i18n="th_actions">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fileTableBody">
|
||||
<tr><td colspan="4" class="text-center" data-i18n="loading_files">Lade Dateien...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONTENT: VPN -->
|
||||
<div id="tabContent_vpn" class="tab-pane">
|
||||
<h2 data-i18n="vpn_title">Slack Nebula Mesh-VPN & Peer-Topologie</h2>
|
||||
<p class="text-muted" data-i18n="vpn_desc">Das P2P Mesh-Overlay verbindet alle autorisierten Endgeräte verschlüsselt ohne Portweiterleitungen am Router.</p>
|
||||
<div class="peer-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="th_peer_name">Node / Peer Name</th>
|
||||
<th data-i18n="th_overlay_ip">Overlay IP</th>
|
||||
<th data-i18n="th_role">Rolle</th>
|
||||
<th data-i18n="th_status">Status</th>
|
||||
<th data-i18n="th_valid_until">Zertifikat gültig bis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="peerTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONTENT: DNS -->
|
||||
<div id="tabContent_dns" class="tab-pane">
|
||||
<h2 data-i18n="dns_title">Custom DNS Server & Subdomain-Routing</h2>
|
||||
<div class="dns-grid">
|
||||
<div class="dns-card">
|
||||
<h3 data-i18n="dns_records_title">Autoritative System-Subdomains</h3>
|
||||
<div id="dnsRecordsList" class="records-list"></div>
|
||||
</div>
|
||||
<div class="dns-card">
|
||||
<h3 data-i18n="dns_logs_title">Live DNS-Abfragen (RFC 1035)</h3>
|
||||
<div id="dnsLogsList" class="logs-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONTENT: PLUGINS -->
|
||||
<div id="tabContent_plugins" class="tab-pane">
|
||||
<h2 data-i18n="plugins_title">Modulare Enterprise-Plugins</h2>
|
||||
<p class="text-muted" data-i18n="plugins_desc">Der Server orchestriert Module in getrennten Prozessen über token-gesicherte Schnittstellen.</p>
|
||||
<div id="pluginCardsGrid" class="plugins-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONTENT: USERS -->
|
||||
<div id="tabContent_users" class="tab-pane">
|
||||
<div class="users-header">
|
||||
<h2 data-i18n="users_title">Benutzer- und Rechteverwaltung</h2>
|
||||
<button class="btn-primary" onclick="openCreateUserModal()" data-i18n="btn_create_user">➕ Neuer Benutzer</button>
|
||||
</div>
|
||||
<div class="users-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="th_user">Benutzer</th>
|
||||
<th data-i18n="th_overlay_ip">Overlay IP</th>
|
||||
<th data-i18n="th_perms">Berechtigungen</th>
|
||||
<th data-i18n="th_base_perms">Basis-Privilegien (Fixiert)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- CREATE USER MODAL -->
|
||||
<div id="createUserModal" class="modal-backdrop" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<h3 data-i18n="modal_create_user_title">Neuen Benutzer anlegen</h3>
|
||||
<form id="createUserForm" onsubmit="submitCreateUser(event)">
|
||||
<div class="form-group">
|
||||
<label data-i18n="lbl_username">Benutzername:</label>
|
||||
<input type="text" id="newUsername" required placeholder="z. B. emil">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="lbl_password">Passwort:</label>
|
||||
<input type="password" id="newPassword" required placeholder="••••••••" minlength="8">
|
||||
</div>
|
||||
<div class="perms-group">
|
||||
<label class="check-label"><input type="checkbox" id="permDrive" checked> Drive (Cloud-Dateien)</label>
|
||||
<label class="check-label"><input type="checkbox" id="permMail" checked> Mail (E-Mail)</label>
|
||||
<label class="check-label"><input type="checkbox" id="permChat" checked> Chat (Team-Chat)</label>
|
||||
<label class="check-label"><input type="checkbox" id="permWeb" checked> Web (Web-Hosting)</label>
|
||||
</div>
|
||||
<div class="immutable-perms-notice">
|
||||
<span class="badge-lock">🔒</span>
|
||||
<span data-i18n="immutable_perms_text"><strong>VPN & DNS</strong> sind unveränderlich aktiv und können nicht entzogen werden.</span>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" onclick="closeCreateUserModal()" data-i18n="btn_cancel">Abbrechen</button>
|
||||
<button type="submit" class="btn-primary" data-i18n="btn_save">Erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<span>BenzCloud © 2026 Jeremy Benz • GNU GPLv3 Lizenz • Nebula (MIT)</span>
|
||||
<span class="pre-release-pill" data-i18n="pre_release_pill">Pre-Release / In aktiver Entwicklung</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
681
app/style.css
Normal file
681
app/style.css
Normal file
@ -0,0 +1,681 @@
|
||||
/* BenzCloud Design System & Tokens */
|
||||
:root {
|
||||
--bg-page: #0a0e17;
|
||||
--bg-surface: #121826;
|
||||
--bg-card: #182032;
|
||||
--bg-card-hover: #1e293f;
|
||||
--border-subtle: #243048;
|
||||
--border-focus: #38bdf8;
|
||||
|
||||
--text-main: #f1f5f9;
|
||||
--text-muted: #94a3b8;
|
||||
--text-subtle: #64748b;
|
||||
|
||||
--accent-blue: #38bdf8;
|
||||
--accent-blue-hover: #0284c7;
|
||||
--accent-green: #34d399;
|
||||
--accent-amber: #fbbf24;
|
||||
--accent-red: #f87171;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-card: 0 4px 20px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.app-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
background-color: rgba(56, 189, 248, 0.15);
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-left: 0.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.domain-pill {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--accent-green);
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
background-color: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.btn-lang {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-lang.active {
|
||||
background-color: var(--accent-blue);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Main Layout */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Wizard */
|
||||
.wizard-card, .auth-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 2.5rem;
|
||||
max-width: 680px;
|
||||
margin: 2rem auto;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.wizard-badge {
|
||||
display: inline-block;
|
||||
background-color: rgba(52, 211, 153, 0.15);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(52, 211, 153, 0.3);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wizard-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.75rem;
|
||||
background: linear-gradient(135deg, #fff, var(--text-muted));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.wizard-desc {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-main);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-subtle);
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.wizard-security-notice {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
background-color: rgba(56, 189, 248, 0.08);
|
||||
border: 1px solid rgba(56, 189, 248, 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.sec-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background-color: var(--accent-blue);
|
||||
color: #04101d;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-blue-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--bg-card);
|
||||
color: var(--text-main);
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
border-color: var(--text-main);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Metrics Grid */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.m-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.m-value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.m-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-subtle);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.text-green { color: var(--accent-green); }
|
||||
.text-blue { color: var(--accent-blue); }
|
||||
|
||||
.pulse-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 8px var(--accent-green);
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
/* Tab Navigation */
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-main);
|
||||
background-color: var(--bg-card);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--accent-blue);
|
||||
background-color: rgba(56, 189, 248, 0.12);
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Services Grid */
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-main);
|
||||
transition: transform 0.2s, border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--border-focus);
|
||||
background-color: var(--bg-card);
|
||||
}
|
||||
|
||||
.s-icon {
|
||||
font-size: 2.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.s-info h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.s-url {
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-blue);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.s-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Pairing Box */
|
||||
.pairing-box {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.pairing-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pair-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.pairing-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.p-item {
|
||||
background-color: var(--bg-card);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-green);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.data-table th, .data-table td {
|
||||
padding: 0.85rem 1.25rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
/* Badges & Tags */
|
||||
.badge-tag {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background-color: rgba(52, 211, 153, 0.15);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background-color: rgba(56, 189, 248, 0.15);
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
.badge-locked {
|
||||
background-color: rgba(52, 211, 153, 0.2);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(52, 211, 153, 0.4);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* DNS Grid */
|
||||
.dns-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.dns-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.dns-card h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.records-list, .logs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.record-row, .log-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 0.8rem;
|
||||
background-color: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Plugins Grid */
|
||||
.plugins-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.p-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 200;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 2rem;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.perms-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.immutable-perms-notice {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background-color: rgba(52, 211, 153, 0.1);
|
||||
border: 1px solid rgba(52, 211, 153, 0.25);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-green);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding: 1.25rem 2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-subtle);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pre-release-pill {
|
||||
background-color: rgba(251, 191, 36, 0.12);
|
||||
color: var(--accent-amber);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
4
favicon.svg
Normal file
4
favicon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="8" fill="#0e121b"/>
|
||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 7.58 4 4 7.58 4 12c0 .53.05 1.05.15 1.55C1.84 14.16 0 16.36 0 19c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" fill="#38bdf8" transform="translate(4, 3)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |
357
index.html
Normal file
357
index.html
Normal file
@ -0,0 +1,357 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BenzCloud Server · Autonome, datenschutzfreundliche Cloud-Suite in Go</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Navigation Header -->
|
||||
<header class="site-header">
|
||||
<div class="nav-container">
|
||||
<div class="nav-left">
|
||||
<a href="https://benzjeremy.github.io/" class="back-root-btn">
|
||||
<span>← benzjeremy.github.io</span>
|
||||
</a>
|
||||
<a href="index.html" class="brand-link">
|
||||
<span class="brand-logo">☁️</span>
|
||||
<span class="brand-name">BenzCloud Server</span>
|
||||
<span class="brand-ver">v1.0</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="main-nav">
|
||||
<a href="app/">
|
||||
<span class="lang-de">Web-Cockpit</span>
|
||||
<span class="lang-en" style="display:none;">Web Cockpit</span>
|
||||
</a>
|
||||
<a href="#features">
|
||||
<span class="lang-de">Features</span>
|
||||
<span class="lang-en" style="display:none;">Features</span>
|
||||
</a>
|
||||
<a href="#architektur">
|
||||
<span class="lang-de">Architektur</span>
|
||||
<span class="lang-en" style="display:none;">Architecture</span>
|
||||
</a>
|
||||
<a href="#downloads">
|
||||
<span class="lang-de">Downloads</span>
|
||||
<span class="lang-en" style="display:none;">Downloads</span>
|
||||
</a>
|
||||
<a href="#about">
|
||||
<span class="lang-de">Über das Projekt</span>
|
||||
<span class="lang-en" style="display:none;">About</span>
|
||||
</a>
|
||||
|
||||
<div class="lang-switch" role="group" aria-label="Sprachauswahl">
|
||||
<button type="button" class="lang-btn active" id="btn-lang-de" onclick="setLang('de')">DE</button>
|
||||
<button type="button" class="lang-btn" id="btn-lang-en" onclick="setLang('en')">EN</button>
|
||||
</div>
|
||||
|
||||
<a href="https://github.com/benzjeremy/benzcloud-server" target="_blank" rel="noopener noreferrer" class="btn-github">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="badge-pill">
|
||||
<span>🛡️</span>
|
||||
<span class="lang-de">Souveräne Lokale Cloud · 100% Datenschutz</span>
|
||||
<span class="lang-en" style="display:none;">Sovereign Local Cloud · 100% Privacy</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="lang-de">Private Enterprise-Suite <span>ohne Cloud-Zwang</span></span>
|
||||
<span class="lang-en" style="display:none;">Private Enterprise Suite <span>Without Cloud Lock-in</span></span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
<span class="lang-de">Die schlanke Open-Source-Alternative zu Nextcloud und Microsoft 365 komplett in reinem Go. Vollautomatisches Mesh-VPN ohne Router-Portfreigaben, eigener DNS-Server, AES-256-GCM Dateitresor und modulare Plugins für Web, Mail und Chat.</span>
|
||||
<span class="lang-en" style="display:none;">The lightweight open-source alternative to Nextcloud and Microsoft 365 written entirely in pure Go. Autonomous Mesh-VPN without router port forwarding, embedded DNS server, AES-256-GCM file vault, and modular plugins for Web, Mail, and Chat.</span>
|
||||
</p>
|
||||
<div class="hero-cta">
|
||||
<a href="app/" class="btn-primary">
|
||||
<span class="lang-de">🚀 Live Web-Cockpit öffnen</span>
|
||||
<span class="lang-en" style="display:none;">🚀 Open Live Web Cockpit</span>
|
||||
</a>
|
||||
<a href="#downloads" class="btn-secondary">
|
||||
<span class="lang-de">📥 Downloads (PC & Android)</span>
|
||||
<span class="lang-en" style="display:none;">📥 Downloads (PC & Android)</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="section" id="features">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="lang-de">Leistungsstarke Kernfeatures</span>
|
||||
<span class="lang-en" style="display:none;">Powerful Core Features</span>
|
||||
</h2>
|
||||
<p class="section-desc">
|
||||
<span class="lang-de">Entwickelt für maximale Einfachheit, Zero-Telemetry und vollständige Datenhoheit auf Ihren eigenen Geräten.</span>
|
||||
<span class="lang-en" style="display:none;">Engineered for zero friction, zero telemetry, and complete data ownership on your own devices.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-3">
|
||||
<div class="card">
|
||||
<div class="card-icon">🌐</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Slack Nebula Mesh-VPN</span>
|
||||
<span class="lang-en" style="display:none;">Slack Nebula Mesh VPN</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Sichere Peer-to-Peer-Verschlüsselung via Noise-Protokoll. Funktioniert out-of-the-box hinter restriktiven Firewalls und NATs – ganz ohne Router-Portfreigaben.</span>
|
||||
<span class="lang-en" style="display:none;">Encrypted peer-to-peer mesh networking powered by the Noise Protocol. Connects seamlessly behind strict NATs and firewalls without opening router ports.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-icon">🧭</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Integrierter DNS-Server</span>
|
||||
<span class="lang-en" style="display:none;">Embedded RFC 1035 DNS</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Eigener DNS-Daemon auf Port 53 (mit automatischem unprivilegiertem Fallback). Löst Wunschdomains und Subdomains wie <code>drive.domain</code> oder <code>chat.domain</code> intern auf.</span>
|
||||
<span class="lang-en" style="display:none;">Built-in DNS daemon on port 53 (with automatic non-root fallback). Maps custom domains and subdomains like <code>drive.domain</code> or <code>chat.domain</code> locally.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-icon">🔐</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Zero-Dummy-Security</span>
|
||||
<span class="lang-en" style="display:none;">Zero-Dummy Security</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Kryptografisch kompromisslos: AES-256-GCM, PBKDF2 mit 100.000 Runden, 32-Byte CSPRNG-Sitzungstokens und unentziehbare Mesh-VPN-Rechte für alle Benutzer.</span>
|
||||
<span class="lang-en" style="display:none;">Cryptographically uncompromising: AES-256-GCM authenticated encryption, PBKDF2 with 100,000 iterations, 32-byte CSPRNG tokens, and immutable VPN privileges.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-icon">📂</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Verschlüsselter Dateitresor</span>
|
||||
<span class="lang-en" style="display:none;">Encrypted File Vault</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Lokaler Cloud-Speicher mit Versionshistorie, Dateiuploads, Ordnerstrukturen und End-to-End-Verschlüsselung auf Blockebene.</span>
|
||||
<span class="lang-en" style="display:none;">Private cloud storage featuring versioning, direct file uploads, folder hierarchies, and block-level cryptographic storage.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-icon">🧩</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Plugin-Ökosystem</span>
|
||||
<span class="lang-en" style="display:none;">Plugin Ecosystem</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Modulare Erweiterbarkeit durch autonome Plugins: Webhosting (Astro/PHP), E-Mail (SMTP/IMAP) und Echtzeit-Chat mit automatischer Discovery via IPC.</span>
|
||||
<span class="lang-en" style="display:none;">Modular architecture with autonomous plugins: Web hosting (Astro/PHP), Email (SMTP/IMAP), and real-time Chat with auto-discovery over local IPC.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<h3 class="card-title">
|
||||
<span class="lang-de">Multi-Plattform</span>
|
||||
<span class="lang-en" style="display:none;">Multi-Platform</span>
|
||||
</h3>
|
||||
<p class="card-desc">
|
||||
<span class="lang-de">Single-Binary-Ausführung auf Linux und Windows sowie eine schlanke native Android-Server-App (ohne Gradle- oder JAR-Ballast, direkt kompiliert).</span>
|
||||
<span class="lang-en" style="display:none;">Single-binary deployments for Linux and Windows alongside a lightweight native Android server application without build bloat.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Architecture Section -->
|
||||
<section class="section" id="architektur">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="lang-de">Technische Architektur</span>
|
||||
<span class="lang-en" style="display:none;">Technical Architecture</span>
|
||||
</h2>
|
||||
<p class="section-desc">
|
||||
<span class="lang-de">Modulare Komponentenarchitektur mit Go-Kern, SQLite-Persistenz und dezentraler P2P-Koordination.</span>
|
||||
<span class="lang-en" style="display:none;">Modular component architecture powered by Go, SQLite persistence, and decentralized peer coordination.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="arch-card">
|
||||
<h3 style="font-size: 1.3rem; margin-bottom: 1rem; color: var(--accent-primary);">
|
||||
<span class="lang-de">Systemschichten & Datenfluss</span>
|
||||
<span class="lang-en" style="display:none;">System Layers & Data Flow</span>
|
||||
</h3>
|
||||
<div class="arch-grid">
|
||||
<div class="arch-item">
|
||||
<h4>Mesh VPN Core</h4>
|
||||
<p>
|
||||
<span class="lang-de">Slack Nebula Lighthouse- und Node-Konfiguratoren, CA-Zertifikatsausstellung und Peer-Erkennung ohne zentrale Server.</span>
|
||||
<span class="lang-en" style="display:none;">Slack Nebula Lighthouse and node orchestrator, CA issuance, and peer discovery without centralized intermediaries.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="arch-item">
|
||||
<h4>RFC 1035 DNS Daemon</h4>
|
||||
<p>
|
||||
<span class="lang-de">UDP/TCP Port 53 Handler für Mesh-Subdomains. Automatischer Fallback auf 5353 bei unprivilegierten Umgebungen.</span>
|
||||
<span class="lang-en" style="display:none;">UDP/TCP Port 53 handler resolving mesh subdomains. Seamless 5353 fallback when running without root privileges.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="arch-item">
|
||||
<h4>Vhost Reverse Proxy</h4>
|
||||
<p>
|
||||
<span class="lang-de">Dynamisches Routing eingehender HTTP-Anfragen basierend auf Host-Headern an die jeweiligen Subdienste und Plugins.</span>
|
||||
<span class="lang-en" style="display:none;">Dynamic reverse proxy routing inbound HTTP requests by Host header to matching internal subsystems and plugins.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="arch-item">
|
||||
<h4>Plugin IPC Supervisor</h4>
|
||||
<p>
|
||||
<span class="lang-de">Überwacht externe Plugin-Prozesse, fragt periodisch <code>/health</code> Endpunkte ab und aktualisiert DNS-Routing-Einträge.</span>
|
||||
<span class="lang-en" style="display:none;">Monitors plugin child processes, periodically checks <code>/health</code> endpoints, and synchronizes DNS routing maps.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Downloads Section -->
|
||||
<section class="section" id="downloads">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="lang-de">Pakete & Downloads</span>
|
||||
<span class="lang-en" style="display:none;">Packages & Downloads</span>
|
||||
</h2>
|
||||
<p class="section-desc">
|
||||
<span class="lang-de">Alle offiziellen Builds für Linux, Windows und Android. Kryptografisch verifiziert über SHA-256 Prüfsummen.</span>
|
||||
<span class="lang-en" style="display:none;">Official verified binaries for Linux, Windows, and Android with cryptographic SHA-256 integrity verification.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="download-grid">
|
||||
<div class="download-card">
|
||||
<div class="download-icon">🐧</div>
|
||||
<div class="download-title">Linux x86_64</div>
|
||||
<div class="download-meta">Single Binary · ~12.7 MB</div>
|
||||
<a href="https://raw.githubusercontent.com/benzjeremy/benzstore/content/linux/benzcloud-server/benzcloud-server_1.0" class="btn-download">
|
||||
<span class="lang-de">Binary herunterladen</span>
|
||||
<span class="lang-en" style="display:none;">Download Binary</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="download-card">
|
||||
<div class="download-icon">🪟</div>
|
||||
<div class="download-title">Windows x86_64</div>
|
||||
<div class="download-meta">Native Executable · ~13.2 MB</div>
|
||||
<a href="https://raw.githubusercontent.com/benzjeremy/benzstore/content/windows/benzcloud-server/benzcloud-server_1.0.exe" class="btn-download">
|
||||
<span class="lang-de">.exe herunterladen</span>
|
||||
<span class="lang-en" style="display:none;">Download .exe</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="download-card">
|
||||
<div class="download-icon">🤖</div>
|
||||
<div class="download-title">Android APK</div>
|
||||
<div class="download-meta">Signed APK · ~17 KB</div>
|
||||
<a href="https://raw.githubusercontent.com/benzjeremy/benzstore/content/android/benzcloud-server/benzcloud-server_1.0.apk" class="btn-download">
|
||||
<span class="lang-de">APK herunterladen</span>
|
||||
<span class="lang-en" style="display:none;">Download APK</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="download-card">
|
||||
<div class="download-icon">🏪</div>
|
||||
<div class="download-title">BenzStore</div>
|
||||
<div class="download-meta">One-Click Install & Updates</div>
|
||||
<a href="https://benzjeremy.github.io/benzstore/" class="btn-download" style="background-color: var(--accent-primary); color: #000;">
|
||||
<span class="lang-de">Im BenzStore öffnen</span>
|
||||
<span class="lang-en" style="display:none;">Open in BenzStore</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About & Open Source Section -->
|
||||
<section class="section" id="about">
|
||||
<div class="about-box">
|
||||
<div class="about-header">
|
||||
<img src="https://github.com/benzjeremy.png" alt="Jeremy Benz" class="author-avatar">
|
||||
<div class="author-info">
|
||||
<h3>Jeremy Benz (@benzjeremy)</h3>
|
||||
<p>
|
||||
<span class="lang-de">Open-Source-Entwickler & Architekt souveräner Software-Ökosysteme</span>
|
||||
<span class="lang-en" style="display:none;">Open-Source Developer & Architect of Sovereign Software Ecosystems</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary); font-size: 0.95rem; line-height: 1.6;">
|
||||
<span class="lang-de">BenzCloud wurde geschaffen, um Organisationen und Privatanwendern echte digitale Souveränität zurückzugeben. Keine Telemetrie, keine versteckten Cloud-Abhängigkeiten und kein Tracking. Der Quellcode ist vollständig transparent unter der GNU General Public License v3.0 (GPL-3.0) lizenziert.</span>
|
||||
<span class="lang-en" style="display:none;">BenzCloud was created to restore genuine digital sovereignty to individuals and organizations. Free from telemetry, hidden cloud subscriptions, and surveillance capitalism. The codebase is licensed under the GNU General Public License v3.0 (GPL-3.0).</span>
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<a href="https://github.com/benzjeremy/benzcloud-server/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none; font-weight: 600; font-size: 0.9rem;">
|
||||
⚖️ GNU GPLv3 Lizenz
|
||||
</a>
|
||||
<a href="https://github.com/benzjeremy/benzcloud-server" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none; font-weight: 600; font-size: 0.9rem;">
|
||||
📦 GitHub Repository
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Site Footer -->
|
||||
<footer class="site-footer">
|
||||
<div class="footer-container">
|
||||
<div>
|
||||
© 2026 Jeremy Benz · BenzCloud Ecosystem · GNU GPLv3
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="https://benzjeremy.github.io/impressum.html">
|
||||
<span class="lang-de">Impressum</span>
|
||||
<span class="lang-en" style="display:none;">Legal Notice</span>
|
||||
</a>
|
||||
<a href="https://benzjeremy.github.io/datenschutz.html">
|
||||
<span class="lang-de">Datenschutz</span>
|
||||
<span class="lang-en" style="display:none;">Privacy Policy</span>
|
||||
</a>
|
||||
<a href="https://benzjeremy.github.io/">
|
||||
<span class="lang-de">Hauptseite</span>
|
||||
<span class="lang-en" style="display:none;">Main Hub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function setLang(lang) {
|
||||
localStorage.setItem('benzcloud_lang', lang);
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
const isDe = lang === 'de';
|
||||
document.querySelectorAll('.lang-de').forEach(el => el.style.display = isDe ? '' : 'none');
|
||||
document.querySelectorAll('.lang-en').forEach(el => el.style.display = isDe ? 'none' : '');
|
||||
|
||||
const btnDe = document.getElementById('btn-lang-de');
|
||||
const btnEn = document.getElementById('btn-lang-en');
|
||||
if (btnDe && btnEn) {
|
||||
btnDe.classList.toggle('active', isDe);
|
||||
btnEn.classList.toggle('active', !isDe);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const savedLang = localStorage.getItem('benzcloud_lang') || 'de';
|
||||
setLang(savedLang);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
387
style.css
Normal file
387
style.css
Normal file
@ -0,0 +1,387 @@
|
||||
:root {
|
||||
--bg-page: #080a0f;
|
||||
--bg-surface: #0e121b;
|
||||
--bg-card: #141a26;
|
||||
--bg-card-hover: #1a2232;
|
||||
--border-subtle: rgba(255, 255, 255, 0.08);
|
||||
--border-strong: rgba(255, 255, 255, 0.16);
|
||||
--border-accent: rgba(56, 189, 248, 0.35);
|
||||
|
||||
--accent-primary: #38bdf8;
|
||||
--accent-primary-hover: #7dd3fc;
|
||||
--accent-glow: rgba(56, 189, 248, 0.25);
|
||||
--accent-green: #10b981;
|
||||
--accent-purple: #a855f7;
|
||||
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
|
||||
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 20px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: var(--font-main);
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: rgba(8, 10, 15, 0.9);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.nav-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0.85rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-left { display: flex; align-items: center; gap: 1.25rem; }
|
||||
.back-root-btn {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.back-root-btn:hover { color: var(--text-primary); }
|
||||
.brand-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.brand-logo { font-size: 1.5rem; }
|
||||
.brand-name { font-weight: 700; font-size: 1.15rem; }
|
||||
.brand-ver {
|
||||
font-size: 0.72rem;
|
||||
background: rgba(56, 189, 248, 0.15);
|
||||
color: var(--accent-primary);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.main-nav { display: flex; align-items: center; gap: 1.25rem; }
|
||||
.main-nav a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.main-nav a:hover { color: var(--accent-primary); }
|
||||
|
||||
.lang-switch {
|
||||
display: flex;
|
||||
background-color: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
overflow: hidden;
|
||||
}
|
||||
.lang-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lang-btn.active {
|
||||
background-color: var(--accent-primary);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-github {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-primary) !important;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.btn-github:hover {
|
||||
background-color: var(--bg-card-hover);
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero-section {
|
||||
padding: 5rem 1.5rem 4rem;
|
||||
text-align: center;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.badge-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||
color: var(--accent-primary);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 9999px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.hero-title span {
|
||||
color: var(--accent-primary);
|
||||
text-shadow: 0 0 24px var(--accent-glow);
|
||||
}
|
||||
.hero-subtitle {
|
||||
font-size: 1.15rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2.25rem;
|
||||
max-width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.hero-cta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: var(--accent-primary);
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 14px var(--accent-glow);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--bg-card-hover);
|
||||
border-color: var(--border-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
padding: 4.5rem 1.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.section-header {
|
||||
text-align: center;
|
||||
max-width: 650px;
|
||||
margin: 0 auto 3rem;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.section-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Feature Grid */
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
background-color: var(--bg-card);
|
||||
border-color: var(--border-accent);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.card-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Architecture section */
|
||||
.arch-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.arch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.arch-item {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.arch-item h4 {
|
||||
color: var(--accent-primary);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.arch-item p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Downloads section */
|
||||
.download-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.download-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.download-icon { font-size: 2.5rem; }
|
||||
.download-title { font-size: 1.2rem; font-weight: 700; }
|
||||
.download-meta {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.btn-download {
|
||||
width: 100%;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text-primary);
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-download:hover {
|
||||
background-color: var(--accent-primary);
|
||||
color: #000;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* About / Open Source section */
|
||||
.about-box {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 2.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.about-header { display: flex; align-items: center; gap: 1rem; }
|
||||
.author-avatar {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--accent-primary);
|
||||
}
|
||||
.author-info h3 { font-size: 1.25rem; font-weight: 700; }
|
||||
.author-info p { color: var(--text-muted); font-size: 0.85rem; }
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
background-color: var(--bg-surface);
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
.footer-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.footer-links { display: flex; gap: 1.25rem; }
|
||||
.footer-links a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.footer-links a:hover { color: var(--accent-primary); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-title { font-size: 2.2rem; }
|
||||
.main-nav { display: none; }
|
||||
.footer-container { flex-direction: column; text-align: center; }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user