diff --git a/route.php b/route.php new file mode 100644 index 0000000..9a74408 --- /dev/null +++ b/route.php @@ -0,0 +1,469 @@ +&1'; + $rawOutput = shell_exec($command); + if ($rawOutput === null) { + $error = 'Traceroute konnte nicht ausgeführt werden. Ist das Kommando verfügbar?'; + } else { + $traceData = parseTraceroute($rawOutput); + if (empty($traceData)) { + $error = 'Keine Hops gefunden. Prüfen Sie den Hostnamen oder versuchen Sie es später erneut.'; + } + } + } +} + +if (empty($traceData)) { + $traceData = getSampleTrace(); + if ($error === '') { + $error = 'Es werden Beispieldaten angezeigt. Starten Sie eine Abfrage, um echte Traceroute-Daten zu sehen.'; + } +} + +function parseTraceroute(string $raw): array +{ + $lines = preg_split('/\r?\n/', trim($raw)); + if (!$lines) { + return []; + } + + $hops = []; + foreach ($lines as $line) { + if (preg_match('/^\s*\d+\s+/', $line) !== 1) { + continue; + } + + preg_match_all('/(\d+\.\d+)\s+ms/', $line, $latencyMatches); + $latencies = array_map('floatval', $latencyMatches[1] ?? []); + $avgLatency = !empty($latencies) ? array_sum($latencies) / count($latencies) : null; + + if (preg_match('/^\s*(\d+)\s+([0-9\.\*]+)/', $line, $parts) !== 1) { + continue; + } + + $hopNumber = (int) $parts[1]; + $ip = $parts[2]; + if ($ip === '*') { + $ip = 'Zeitüberschreitung'; + } + + $hops[] = [ + 'hop' => $hopNumber, + 'ip' => $ip, + 'avgLatency' => $avgLatency, + 'raw' => trim($line), + ]; + } + + return $hops; +} + +function getSampleTrace(): array +{ + return [ + ['hop' => 1, 'ip' => '192.168.0.1', 'avgLatency' => 1.2, 'raw' => '1 192.168.0.1 1.123 ms 1.234 ms 1.301 ms'], + ['hop' => 2, 'ip' => '10.12.34.1', 'avgLatency' => 9.4, 'raw' => '2 10.12.34.1 9.123 ms 9.567 ms 9.400 ms'], + ['hop' => 3, 'ip' => '172.16.5.4', 'avgLatency' => 18.7, 'raw' => '3 172.16.5.4 18.432 ms 18.913 ms 18.787 ms'], + ['hop' => 4, 'ip' => '203.0.113.5', 'avgLatency' => 32.9, 'raw' => '4 203.0.113.5 32.113 ms 33.441 ms 33.212 ms'], + ['hop' => 5, 'ip' => '93.184.216.34', 'avgLatency' => 48.2, 'raw' => '5 93.184.216.34 48.112 ms 48.501 ms 48.032 ms'], + ]; +} + +function generatePositions(array $trace): array +{ + $positions = []; + $radius = 25; + $spacing = 10; + foreach ($trace as $index => $hop) { + $angle = $index * 0.9; + $positions[] = [ + 'x' => cos($angle) * $radius, + 'y' => $index * $spacing, + 'z' => sin($angle) * $radius, + ]; + } + return $positions; +} + +$positions = generatePositions($traceData); +?> + + + + + + 3D Traceroute Visualisierung + + + +
+

3D Traceroute Explorer

+

Visualisieren Sie Netzwerkpfade im dreidimensionalen Raum und erkunden Sie die einzelnen Hops.

+
+
+
+ +
+ +
+ + + + + + diff --git a/write.php b/write.php index 3824f03..28addb9 100644 --- a/write.php +++ b/write.php @@ -30,8 +30,6 @@ define('LOG_RETENTION_MONTHS', 6); define('ONLINE_TIMEOUT_SECONDS', 30); define('SSE_RETRY_MS', 500); define('MAX_MESSAGES_PER_FETCH', 200); -define('MAX_ATTACHMENT_SIZE', 200 * 1024); // 200 KB -define('UPLOAD_DIR', __DIR__ . '/uploads'); // Rate Limiting define('MAX_MESSAGES_PER_MINUTE', 10); @@ -73,152 +71,129 @@ $PROFANITY_FILTER = [ // ═══════════════════════════════════════════════════════════ function getDB() { - static $db = null; - static $initialized = false; + $db = new SQLite3(DB_FILE); + $db->busyTimeout(5000); + + // Users Table (mit Geburtsdatum und User-ID) + $db->exec(' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + user_id TEXT UNIQUE NOT NULL, + birthdate DATE NOT NULL, + age_group TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + is_banned INTEGER DEFAULT 0, + ban_reason TEXT + ) + '); + + // Messages Table + $db->exec(' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_user_id INTEGER NOT NULL, + to_user_id INTEGER NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + is_read INTEGER DEFAULT 0, + is_flagged INTEGER DEFAULT 0, + flag_reason TEXT, + FOREIGN KEY (from_user_id) REFERENCES users(id), + FOREIGN KEY (to_user_id) REFERENCES users(id) + ) + '); + + // Online Status Table + $db->exec(' + CREATE TABLE IF NOT EXISTS online_status ( + user_id INTEGER PRIMARY KEY, + last_ping DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); - if ($db === null) { - if (!is_dir(UPLOAD_DIR)) { - @mkdir(UPLOAD_DIR, 0755, true); - } + // User Sessions Table + $db->exec(' + CREATE TABLE IF NOT EXISTS user_sessions ( + user_id INTEGER PRIMARY KEY, + session_token TEXT NOT NULL, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); - $db = new SQLite3(DB_FILE); - $db->busyTimeout(5000); - } - - if (!$initialized) { - // Users Table (mit Geburtsdatum und User-ID) - $db->exec(' - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL, - user_id TEXT UNIQUE NOT NULL, - birthdate DATE NOT NULL, - age_group TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, - is_banned INTEGER DEFAULT 0, - ban_reason TEXT - ) - '); - - // Messages Table - $db->exec(' - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_user_id INTEGER NOT NULL, - to_user_id INTEGER NOT NULL, - message TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - is_read INTEGER DEFAULT 0, - is_flagged INTEGER DEFAULT 0, - flag_reason TEXT, - attachment_path TEXT, - attachment_type TEXT, - attachment_size INTEGER, - FOREIGN KEY (from_user_id) REFERENCES users(id), - FOREIGN KEY (to_user_id) REFERENCES users(id) - ) - '); - - // Ensure attachment columns exist for older installations - $messagesInfo = $db->query('PRAGMA table_info(messages)'); - $messageColumns = []; - while ($column = $messagesInfo->fetchArray(SQLITE3_ASSOC)) { - $messageColumns[$column['name']] = true; - } - if (!isset($messageColumns['attachment_path'])) { - $db->exec('ALTER TABLE messages ADD COLUMN attachment_path TEXT'); - } - if (!isset($messageColumns['attachment_type'])) { - $db->exec('ALTER TABLE messages ADD COLUMN attachment_type TEXT'); - } - if (!isset($messageColumns['attachment_size'])) { - $db->exec('ALTER TABLE messages ADD COLUMN attachment_size INTEGER'); - } - - // Online Status Table - $db->exec(' - CREATE TABLE IF NOT EXISTS online_status ( - user_id INTEGER PRIMARY KEY, - last_ping DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - '); - - // User Sessions Table - $db->exec(' - CREATE TABLE IF NOT EXISTS user_sessions ( - user_id INTEGER PRIMARY KEY, - session_token TEXT NOT NULL, - last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - '); - - // Reports Table - $db->exec(' - CREATE TABLE IF NOT EXISTS reports ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - reporter_id INTEGER NOT NULL, - reported_user_id INTEGER NOT NULL, - reason TEXT NOT NULL, - message_id INTEGER, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - status TEXT DEFAULT "pending", - FOREIGN KEY (reporter_id) REFERENCES users(id), - FOREIGN KEY (reported_user_id) REFERENCES users(id), - FOREIGN KEY (message_id) REFERENCES messages(id) - ) - '); - - // Blocks Table - $db->exec(' - CREATE TABLE IF NOT EXISTS blocks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - blocker_id INTEGER NOT NULL, - blocked_id INTEGER NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (blocker_id) REFERENCES users(id), - FOREIGN KEY (blocked_id) REFERENCES users(id), - UNIQUE(blocker_id, blocked_id) - ) - '); - - // Rate Limiting Table - $db->exec(' - CREATE TABLE IF NOT EXISTS rate_limits ( - user_id INTEGER NOT NULL, - action_type TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - '); - - // Logs Table (für Behörden) - $db->exec(' - CREATE TABLE IF NOT EXISTS security_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - action TEXT NOT NULL, - details TEXT, - ip_address TEXT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - '); - - // Admin Table - $db->exec(' - CREATE TABLE IF NOT EXISTS admins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - '); - - // Create default admin if not exists - $stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username'); + // Reports Table + $db->exec(' + CREATE TABLE IF NOT EXISTS reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reporter_id INTEGER NOT NULL, + reported_user_id INTEGER NOT NULL, + reason TEXT NOT NULL, + message_id INTEGER, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT "pending", + FOREIGN KEY (reporter_id) REFERENCES users(id), + FOREIGN KEY (reported_user_id) REFERENCES users(id), + FOREIGN KEY (message_id) REFERENCES messages(id) + ) + '); + + // Blocks Table + $db->exec(' + CREATE TABLE IF NOT EXISTS blocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blocker_id INTEGER NOT NULL, + blocked_id INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (blocker_id) REFERENCES users(id), + FOREIGN KEY (blocked_id) REFERENCES users(id), + UNIQUE(blocker_id, blocked_id) + ) + '); + + // Rate Limiting Table + $db->exec(' + CREATE TABLE IF NOT EXISTS rate_limits ( + user_id INTEGER NOT NULL, + action_type TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // Logs Table (für Behörden) + $db->exec(' + CREATE TABLE IF NOT EXISTS security_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + action TEXT NOT NULL, + details TEXT, + ip_address TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // Admin Table + $db->exec(' + CREATE TABLE IF NOT EXISTS admins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + '); + + // Create default admin if not exists + $stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username'); + $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] == 0) { + $stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)'); $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT); $result = $stmt->execute(); $row = $result->fetchArray(SQLITE3_ASSOC); @@ -240,6 +215,14 @@ function getDB() { $initialized = true; } + + // Create indexes + $db->exec('CREATE INDEX IF NOT EXISTS idx_messages_users ON messages(from_user_id, to_user_id)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)'); return $db; } @@ -503,7 +486,7 @@ function generateSessionToken() { return bin2hex(random_bytes(32)); } -function startUserSession($userId, $force = false) { +function startUserSession($userId) { if (!$userId) { return ['allowed' => false, 'error' => 'Ungültige Benutzer-ID']; } @@ -514,24 +497,17 @@ function startUserSession($userId, $force = false) { $result = $stmt->execute(); $existing = $result->fetchArray(SQLITE3_ASSOC); - if ($existing && !$force && !empty($existing['last_seen'])) { + if ($existing && !empty($existing['last_seen'])) { $secondsSinceLastSeen = time() - strtotime($existing['last_seen']); if ($secondsSinceLastSeen < ONLINE_TIMEOUT_SECONDS) { return [ 'allowed' => false, - 'error' => 'Du bist bereits auf einem anderen Gerät eingeloggt. Übernimm die Sitzung nur, wenn du wirklich ausgeloggt bist.', - 'can_force' => true + 'error' => 'Du bist bereits auf einem anderen Gerät eingeloggt. Bitte dort zuerst ausloggen oder kurz warten.' ]; } } - if ($force && $existing) { - $stmt = $db->prepare('DELETE FROM user_sessions WHERE user_id = :user_id'); - $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); - $stmt->execute(); - } - $token = generateSessionToken(); $stmt = $db->prepare(' @@ -1006,9 +982,6 @@ if (isset($_POST['action']) || isset($_GET['action'])) { m.timestamp, m.is_read, m.is_flagged, - m.attachment_path, - m.attachment_type, - m.attachment_size, u.username as from_username, u.user_id as from_display_id FROM messages m @@ -1959,7 +1932,6 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: linear-gradient(135deg, #fef08a 0%, #f97316 100%); - background-color: #fff9db; min-height: 100vh; display: flex; align-items: center; @@ -3647,136 +3619,6 @@ const chatStateMessageEl = document.getElementById('chatStateMessage'); const chatMessagesHeaderEl = document.getElementById('chatMessagesHeader'); const chatInputEl = document.getElementById('chatInput'); const sendButtonEl = document.getElementById('sendButton'); -const attachmentButtonEl = document.getElementById('attachmentButton'); -const attachmentInputEl = document.getElementById('attachmentInput'); -const attachmentInfoEl = document.getElementById('attachmentInfo'); -const attachmentFileNameEl = document.getElementById('attachmentFileName'); -const attachmentClearBtnEl = document.getElementById('attachmentClearBtn'); -const attachmentWarningEl = document.getElementById('attachmentWarning'); -const ATTACHMENT_MAX_SIZE = 200 * 1024; -let messageAbortController = null; -let sseErrorCount = 0; -let usePollingFallback = false; -let pollingTimerId = null; -let isPollingUpdates = false; -const POLLING_INTERVAL_MS = 5000; - -function processIncomingMessages(messages) { - if (!Array.isArray(messages) || messages.length === 0) { - return; - } - - let shouldRender = false; - const markReadFor = new Set(); - - messages.forEach(msg => { - const messageId = Number(msg.id); - if (Number.isFinite(messageId) && messageId > state.lastMessageId) { - state.lastMessageId = messageId; - } - - const isRelevantChat = state.selectedUserId && ( - (msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) || - (msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId) - ); - - if (isRelevantChat) { - const alreadyExists = state.messages.some(existing => Number(existing.id) === messageId); - - if (!alreadyExists) { - state.messages.push(msg); - shouldRender = true; - - if (msg.to_user_id === state.currentUserId) { - markReadFor.add(msg.from_user_id); - } - } - } - }); - - if (shouldRender) { - renderMessages(); - } - - markReadFor.forEach(userId => markAsRead(userId)); - - if (messages.length > 0) { - loadUsers(); - } -} - -function stopPollingUpdates() { - if (pollingTimerId) { - clearInterval(pollingTimerId); - pollingTimerId = null; - } -} - -function startPollingUpdates() { - stopPollingUpdates(); - pollMessages(); - pollingTimerId = setInterval(pollMessages, POLLING_INTERVAL_MS); -} - -async function pollMessages() { - if (isPollingUpdates) { - return; - } - - isPollingUpdates = true; - - const formData = new FormData(); - formData.append('action', 'poll_updates'); - formData.append('last_message_id', state.lastMessageId); - - try { - const response = await postFormData(formData); - const result = await response.json(); - - if (result && result.success) { - const messages = Array.isArray(result.messages) ? result.messages : []; - if (typeof result.last_message_id === 'number') { - const newest = Number(result.last_message_id); - if (Number.isFinite(newest)) { - state.lastMessageId = Math.max(state.lastMessageId, newest); - } - } - processIncomingMessages(messages); - } - } catch (error) { - console.warn('Polling fehlgeschlagen:', error); - } finally { - isPollingUpdates = false; - } -} - -function enablePollingFallback() { - if (usePollingFallback) { - return; - } - - usePollingFallback = true; - - if (state.eventSource) { - state.eventSource.close(); - state.eventSource = null; - } - - if (!state.connectionErrorShown && state.selectedUserId && !state.isLoadingMessages) { - updateChatState('error', 'Live-Verbindung blockiert. Wechsel auf sichere Aktualisierung…'); - state.connectionErrorShown = true; - } - - startPollingUpdates(); -} - -function startRealtime() { - if (usePollingFallback) { - startPollingUpdates(); - } else { - startSSE(); - } -} async function loadUsers() { if (!userListEl) { @@ -3787,7 +3629,7 @@ async function loadUsers() { renderUserList(); try { - const response = await fetch(buildUrl({ action: 'get_users' }), { credentials: 'same-origin' }); + const response = await fetch('?action=get_users'); if (!response.ok) { throw new Error('NETZWERK_FEHLER'); } @@ -3836,23 +3678,9 @@ function renderUserList() { return; } - const offlineLimit = 5; - const onlineUsers = []; - const offlineUsers = []; - - filtered.forEach(user => { - if (user.is_online) { - onlineUsers.push(user); - } else { - offlineUsers.push(user); - } - }); - - const limitedUsers = onlineUsers.concat(offlineUsers.slice(0, offlineLimit)); - const fragment = document.createDocumentFragment(); - limitedUsers.forEach(user => { + filtered.forEach(user => { const item = document.createElement('button'); item.type = 'button'; item.className = 'user-item' + (Number(user.id) === Number(state.selectedUserId) ? ' active' : ''); @@ -3900,15 +3728,6 @@ function renderUserList() { userListEl.innerHTML = ''; userListEl.appendChild(fragment); - - if (offlineUsers.length > offlineLimit) { - const hint = document.createElement('div'); - hint.className = 'user-status'; - hint.style.textAlign = 'center'; - hint.style.marginTop = '12px'; - hint.textContent = 'Weitere Offline-Nutzer werden ausgeblendet.'; - userListEl.appendChild(hint); - } } function renderChatHeader(displayName) { @@ -3966,9 +3785,6 @@ function selectUser(userId, displayName) { state.selectedUserId = userId; state.messages = []; - clearAttachmentSelection(); - clearAttachmentWarning(); - if (chatWelcomeEl) { chatWelcomeEl.style.display = 'none'; } @@ -3993,21 +3809,11 @@ async function loadMessages(userId) { return; } - if (messageAbortController) { - messageAbortController.abort(); - } - - const currentController = new AbortController(); - messageAbortController = currentController; - state.isLoadingMessages = true; updateChatState('loading', 'Nachrichten werden geladen…'); try { - const response = await fetch(buildUrl({ action: 'get_messages', user_id: userId }), { - signal: currentController.signal, - credentials: 'same-origin' - }); + const response = await fetch(`?action=get_messages&user_id=${userId}`); if (!response.ok) { throw new Error('NETZWERK_FEHLER'); } @@ -4032,9 +3838,6 @@ async function loadMessages(userId) { updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!'); } } catch (error) { - if (error && error.name === 'AbortError') { - return; - } console.error('Nachrichten konnten nicht geladen werden:', error); state.messages = []; if (chatMessagesEl) { @@ -4045,10 +3848,7 @@ async function loadMessages(userId) { : 'Nachrichten konnten nicht geladen werden. Bitte versuche es erneut.'; updateChatState('error', errorMessage); } finally { - if (messageAbortController === currentController) { - messageAbortController = null; - state.isLoadingMessages = false; - } + state.isLoadingMessages = false; } } @@ -4089,51 +3889,12 @@ function renderMessages() { updateChatState(null); } -function clearAttachmentSelection() { - if (attachmentInputEl) { - attachmentInputEl.value = ''; - } - if (attachmentInfoEl) { - attachmentInfoEl.classList.add('hidden'); - } - if (attachmentFileNameEl) { - attachmentFileNameEl.textContent = ''; - } -} - -function showAttachmentWarning(message) { - if (attachmentWarningEl) { - attachmentWarningEl.textContent = message; - attachmentWarningEl.classList.remove('hidden'); - } else { - alert(message); - } -} - -function clearAttachmentWarning() { - if (attachmentWarningEl) { - attachmentWarningEl.textContent = ''; - attachmentWarningEl.classList.add('hidden'); - } -} - async function sendMessage() { if (!chatInputEl) { return; } - if (!state.selectedUserId) { - showAttachmentWarning('Bitte wähle zuerst einen Chat aus.'); - return; - } - const message = chatInputEl.value.trim(); - const attachmentFile = attachmentInputEl?.files?.[0] || null; - - if (!message && !attachmentFile) { - showAttachmentWarning('Bitte gib eine Nachricht ein oder hänge ein JPG-Bild an.'); - return; - } clearAttachmentWarning(); @@ -4164,21 +3925,11 @@ async function sendMessage() { formData.append('attachment', attachmentFile); } - try { - const response = await postFormData(formData); - const result = await response.json(); - - if (result.success) { - chatInputEl.value = ''; - chatInputEl.dispatchEvent(new Event('input')); - clearAttachmentSelection(); - clearAttachmentWarning(); - } else { - showAttachmentWarning(result.error || 'Nachricht konnte nicht gesendet werden.'); - } - } catch (error) { - console.error('Nachricht konnte nicht gesendet werden:', error); - showAttachmentWarning('Nachricht konnte nicht gesendet werden.'); + if (result.success) { + chatInputEl.value = ''; + chatInputEl.dispatchEvent(new Event('input')); + } else { + alert(result.error); } } @@ -4192,25 +3943,14 @@ async function markAsRead(userId) { } function startSSE() { - stopPollingUpdates(); - if (state.eventSource) { state.eventSource.close(); - state.eventSource = null; } - const url = buildUrl({ stream: 'events', last_message_id: state.lastMessageId, t: Date.now() }); - - try { - state.eventSource = new EventSource(url); - } catch (error) { - console.warn('SSE kann nicht gestartet werden, wechsle auf Polling:', error); - enablePollingFallback(); - return; - } + const url = `?stream=events&last_message_id=${state.lastMessageId}&t=${Date.now()}`; + state.eventSource = new EventSource(url); state.eventSource.onopen = () => { - sseErrorCount = 0; state.connectionErrorShown = false; if (!state.selectedUserId) { @@ -4230,10 +3970,30 @@ function startSSE() { state.eventSource.onmessage = (event) => { state.connectionErrorShown = false; + const data = JSON.parse(event.data); - if (!event.data) { - return; - } + if (data.type === 'messages' && data.messages) { + data.messages.forEach(msg => { + const messageId = Number(msg.id); + + if (messageId > state.lastMessageId) { + state.lastMessageId = messageId; + + if (state.selectedUserId && + ((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) || + (msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) { + + if (!state.messages.find(m => Number(m.id) === messageId)) { + state.messages.push(msg); + renderMessages(); + + if (msg.to_user_id === state.currentUserId) { + markAsRead(msg.from_user_id); + } + } + } + } + }); try { const data = JSON.parse(event.data); @@ -4246,13 +4006,6 @@ function startSSE() { }; state.eventSource.onerror = () => { - if (state.eventSource) { - state.eventSource.close(); - state.eventSource = null; - } - - sseErrorCount += 1; - if (!state.connectionErrorShown) { state.connectionErrorShown = true; console.warn('SSE-Verbindung unterbrochen, versuche Neuverbindung.'); @@ -4261,16 +4014,11 @@ function startSSE() { } } - if (usePollingFallback || sseErrorCount >= 3) { - enablePollingFallback(); - return; + if (state.eventSource) { + state.eventSource.close(); } - setTimeout(() => { - if (!usePollingFallback) { - startSSE(); - } - }, 500); + setTimeout(startSSE, 1500); }; } @@ -4280,56 +4028,6 @@ function escapeHtml(text) { return div.innerHTML; } -function escapeAttribute(value) { - const div = document.createElement('div'); - div.textContent = value ?? ''; - return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); -} - -attachmentButtonEl?.addEventListener('click', () => { - attachmentInputEl?.click(); -}); - -attachmentInputEl?.addEventListener('change', () => { - clearAttachmentWarning(); - - if (!attachmentInputEl.files || attachmentInputEl.files.length === 0) { - clearAttachmentSelection(); - return; - } - - const file = attachmentInputEl.files[0]; - const fileType = (file.type || '').toLowerCase(); - const fileName = file.name || ''; - const isJpeg = /^image\/jpe?g$/.test(fileType) || /\.jpe?g$/i.test(fileName); - - if (!isJpeg) { - showAttachmentWarning('Nur JPG-Bilder sind erlaubt.'); - clearAttachmentSelection(); - return; - } - - if (file.size > ATTACHMENT_MAX_SIZE) { - showAttachmentWarning('Bild ist zu groß (max. 200 KB).'); - clearAttachmentSelection(); - return; - } - - if (attachmentInfoEl) { - attachmentInfoEl.classList.remove('hidden'); - } - - if (attachmentFileNameEl) { - const sizeKb = Math.max(1, Math.round(file.size / 1024)); - attachmentFileNameEl.textContent = `${file.name} (${sizeKb} KB)`; - } -}); - -attachmentClearBtnEl?.addEventListener('click', () => { - clearAttachmentSelection(); - clearAttachmentWarning(); -}); - sendButtonEl?.addEventListener('click', sendMessage); chatInputEl?.addEventListener('keypress', (e) => {