From cc439c8d0c7cb218f2ff398ec64849dcb8d41341 Mon Sep 17 00:00:00 2001 From: NiekRabelink Date: Fri, 11 Sep 2026 13:00:56 +0000 Subject: [PATCH 1/9] Update config/.env --- config/{.env.example => .env} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/{.env.example => .env} (100%) diff --git a/config/.env.example b/config/.env similarity index 100% rename from config/.env.example rename to config/.env From 29b8ce9211c6ff8cd32f2f43f813e49725ef42c1 Mon Sep 17 00:00:00 2001 From: NiekRabelink Date: Fri, 11 Sep 2026 13:03:51 +0000 Subject: [PATCH 2/9] Update config/.env --- config/.env | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/config/.env b/config/.env index b930d6c..0a4477d 100644 --- a/config/.env +++ b/config/.env @@ -1,38 +1,33 @@ # ========================================================================= -# FOCIZ OMGEVINGSCONFIGURATIE VOORBEELD (TEMPLATE) +# FOCIZ CONFIGURATIE — LIVE PRODUCTIE (www.fociz.nl) # ========================================================================= -# Alle actieve database- en applicatie-instellingen staan centraal in: -# config/config.php -# -# Indien gewenst kan een lokaal .env bestand in de hoofdmap worden geplaatst -# om specifieke variabelen lokaal te overschrijven. -DB_HOST="127.0.0.1" +# MariaDB Verbinding +DB_HOST="192.168.2.33" DB_PORT="3306" DB_NAME="fociz" -DB_USER="fociz_user" -DB_PASS="" +DB_USER="focizlive_user" +DB_PASS="F0c1z_Live_Secure_2026!" +# Omgeving & URL APP_ENV="live" APP_URL="https://www.fociz.nl" -APP_SECRET="" -MICROSOFT_CLIENT_ID="" -MICROSOFT_CLIENT_SECRET="" -MICROSOFT_TENANT_ID="" +# Applicatie Geheime Sleutel (voor tokenversleuteling & OAuth state) +APP_SECRET="fociz-maria-db-super-secret-key-998877" -JIRA_DOMAIN="" -JIRA_EMAIL="" -JIRA_API_TOKEN="" +# Microsoft 365 Entra ID Tenant +MICROSOFT_TENANT_ID="planningsagenda.nl" # SMTP Mail Server SMTP_HOST="mail.fociz.nl" SMTP_PORT="587" SMTP_USER="info@fociz.nl" -SMTP_PASS="" +SMTP_PASS="6swpQSBg5uWFmjar7F6a" SMTP_SECURE="tls" -SMTP_FROM_EMAIL="no-reply@fociz.nl" +SMTP_FROM_EMAIL="info@fociz.nl" SMTP_FROM_NAME="Fociz" SMTP_REPLY_TO="info@fociz.nl" - +# Webhooks (optioneel) +JIRA_WEBHOOK_SECRET="fociz_webhook_live_2026" \ No newline at end of file From 0092cc17ef81f246cb9f4c12e93864f802ad6b12 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Fri, 11 Sep 2026 15:11:03 +0200 Subject: [PATCH 3/9] refactor(config): remove config/.env from git tracking, restore config/.env.example, and update .gitignore --- .gitignore | 8 +++++++- config/.env | 33 --------------------------------- config/.env.example | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 34 deletions(-) delete mode 100644 config/.env create mode 100644 config/.env.example diff --git a/.gitignore b/.gitignore index 6b94ae6..c7e0bff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ -# Environment +# Environment & Secrets .env +.env.* .env*.local +*.env +config/.env +config/.env.* +!.env.example +!config/.env.example # Systeem & Editor .DS_Store diff --git a/config/.env b/config/.env deleted file mode 100644 index 0a4477d..0000000 --- a/config/.env +++ /dev/null @@ -1,33 +0,0 @@ -# ========================================================================= -# FOCIZ CONFIGURATIE — LIVE PRODUCTIE (www.fociz.nl) -# ========================================================================= - -# MariaDB Verbinding -DB_HOST="192.168.2.33" -DB_PORT="3306" -DB_NAME="fociz" -DB_USER="focizlive_user" -DB_PASS="F0c1z_Live_Secure_2026!" - -# Omgeving & URL -APP_ENV="live" -APP_URL="https://www.fociz.nl" - -# Applicatie Geheime Sleutel (voor tokenversleuteling & OAuth state) -APP_SECRET="fociz-maria-db-super-secret-key-998877" - -# Microsoft 365 Entra ID Tenant -MICROSOFT_TENANT_ID="planningsagenda.nl" - -# SMTP Mail Server -SMTP_HOST="mail.fociz.nl" -SMTP_PORT="587" -SMTP_USER="info@fociz.nl" -SMTP_PASS="6swpQSBg5uWFmjar7F6a" -SMTP_SECURE="tls" -SMTP_FROM_EMAIL="info@fociz.nl" -SMTP_FROM_NAME="Fociz" -SMTP_REPLY_TO="info@fociz.nl" - -# Webhooks (optioneel) -JIRA_WEBHOOK_SECRET="fociz_webhook_live_2026" \ No newline at end of file diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..b930d6c --- /dev/null +++ b/config/.env.example @@ -0,0 +1,38 @@ +# ========================================================================= +# FOCIZ OMGEVINGSCONFIGURATIE VOORBEELD (TEMPLATE) +# ========================================================================= +# Alle actieve database- en applicatie-instellingen staan centraal in: +# config/config.php +# +# Indien gewenst kan een lokaal .env bestand in de hoofdmap worden geplaatst +# om specifieke variabelen lokaal te overschrijven. + +DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_NAME="fociz" +DB_USER="fociz_user" +DB_PASS="" + +APP_ENV="live" +APP_URL="https://www.fociz.nl" +APP_SECRET="" + +MICROSOFT_CLIENT_ID="" +MICROSOFT_CLIENT_SECRET="" +MICROSOFT_TENANT_ID="" + +JIRA_DOMAIN="" +JIRA_EMAIL="" +JIRA_API_TOKEN="" + +# SMTP Mail Server +SMTP_HOST="mail.fociz.nl" +SMTP_PORT="587" +SMTP_USER="info@fociz.nl" +SMTP_PASS="" +SMTP_SECURE="tls" +SMTP_FROM_EMAIL="no-reply@fociz.nl" +SMTP_FROM_NAME="Fociz" +SMTP_REPLY_TO="info@fociz.nl" + + From 6a9935e5cf8bfea12bc3bd2c2e74eb803e192336 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Fri, 11 Sep 2026 15:17:17 +0200 Subject: [PATCH 4/9] fix(config): restore live config/.env and add DB connection safety net --- .gitignore | 14 +++++--------- config/.env | 33 +++++++++++++++++++++++++++++++++ config/.env.example | 38 -------------------------------------- config/config.php | 4 ++-- 4 files changed, 40 insertions(+), 49 deletions(-) create mode 100644 config/.env delete mode 100644 config/.env.example diff --git a/.gitignore b/.gitignore index c7e0bff..0169e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,8 @@ -# Environment & Secrets -.env -.env.* -.env*.local -*.env -config/.env -config/.env.* -!.env.example -!config/.env.example +# Environment +/.env +/.env.local +*.local +!config/.env # Systeem & Editor .DS_Store diff --git a/config/.env b/config/.env new file mode 100644 index 0000000..da1d658 --- /dev/null +++ b/config/.env @@ -0,0 +1,33 @@ +# ========================================================================= +# FOCIZ CONFIGURATIE — LIVE PRODUCTIE (www.fociz.nl) +# ========================================================================= + +# MariaDB Verbinding +DB_HOST="192.168.2.33" +DB_PORT="3306" +DB_NAME="fociz" +DB_USER="focizlive_user" +DB_PASS="F0c1z_Live_Secure_2026!" + +# Omgeving & URL +APP_ENV="live" +APP_URL="https://www.fociz.nl" + +# Applicatie Geheime Sleutel (voor tokenversleuteling & OAuth state) +APP_SECRET="fociz-maria-db-super-secret-key-998877" + +# Microsoft 365 Entra ID Tenant +MICROSOFT_TENANT_ID="planningsagenda.nl" + +# SMTP Mail Server +SMTP_HOST="mail.fociz.nl" +SMTP_PORT="587" +SMTP_USER="info@fociz.nl" +SMTP_PASS="6swpQSBg5uWFmjar7F6a" +SMTP_SECURE="tls" +SMTP_FROM_EMAIL="info@fociz.nl" +SMTP_FROM_NAME="Fociz" +SMTP_REPLY_TO="info@fociz.nl" + +# Webhooks (optioneel) +JIRA_WEBHOOK_SECRET="fociz_webhook_live_2026" diff --git a/config/.env.example b/config/.env.example deleted file mode 100644 index b930d6c..0000000 --- a/config/.env.example +++ /dev/null @@ -1,38 +0,0 @@ -# ========================================================================= -# FOCIZ OMGEVINGSCONFIGURATIE VOORBEELD (TEMPLATE) -# ========================================================================= -# Alle actieve database- en applicatie-instellingen staan centraal in: -# config/config.php -# -# Indien gewenst kan een lokaal .env bestand in de hoofdmap worden geplaatst -# om specifieke variabelen lokaal te overschrijven. - -DB_HOST="127.0.0.1" -DB_PORT="3306" -DB_NAME="fociz" -DB_USER="fociz_user" -DB_PASS="" - -APP_ENV="live" -APP_URL="https://www.fociz.nl" -APP_SECRET="" - -MICROSOFT_CLIENT_ID="" -MICROSOFT_CLIENT_SECRET="" -MICROSOFT_TENANT_ID="" - -JIRA_DOMAIN="" -JIRA_EMAIL="" -JIRA_API_TOKEN="" - -# SMTP Mail Server -SMTP_HOST="mail.fociz.nl" -SMTP_PORT="587" -SMTP_USER="info@fociz.nl" -SMTP_PASS="" -SMTP_SECURE="tls" -SMTP_FROM_EMAIL="no-reply@fociz.nl" -SMTP_FROM_NAME="Fociz" -SMTP_REPLY_TO="info@fociz.nl" - - diff --git a/config/config.php b/config/config.php index 78e441d..e04cbbf 100644 --- a/config/config.php +++ b/config/config.php @@ -104,7 +104,7 @@ define('DB_HOST', fociz_env('DB_HOST', '192.168.2.33')); define('DB_PORT', fociz_env('DB_PORT', '3306')); define('DB_NAME', fociz_env('DB_NAME', $isDevEnv ? 'focizdev' : 'fociz')); define('DB_USER', fociz_env('DB_USER', $isDevEnv ? 'focizdev_user' : 'focizlive_user')); -define('DB_PASS', fociz_env('DB_PASS', '')); +define('DB_PASS', fociz_env('DB_PASS', $isDevEnv ? 'F0c1z_Dev_Secure_2026!' : 'F0c1z_Live_Secure_2026!')); define('DB_CHARSET', 'utf8mb4'); // Bepaal de dynamische of geconfigureerde applicatie URL @@ -141,7 +141,7 @@ date_default_timezone_set('Europe/Amsterdam'); define('SMTP_HOST', fociz_env('SMTP_HOST', 'mail.fociz.nl')); define('SMTP_PORT', (int)fociz_env('SMTP_PORT', 587)); define('SMTP_USER', fociz_env('SMTP_USER', 'info@fociz.nl')); -define('SMTP_PASS', fociz_env('SMTP_PASS', '')); +define('SMTP_PASS', fociz_env('SMTP_PASS', '6swpQSBg5uWFmjar7F6a')); define('SMTP_SECURE', fociz_env('SMTP_SECURE', 'tls')); // 'tls' (STARTTLS port 587) of 'ssl' (port 465) define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'info@fociz.nl')); define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz')); From 32974cdb31f7ce204c3869590e7bb05f4f48a0fb Mon Sep 17 00:00:00 2001 From: Darkayer Date: Wed, 23 Sep 2026 09:21:17 +0200 Subject: [PATCH 5/9] Enhance IP verification process: add email resend functionality and improve error logging --- auth/login.php | 91 ++++++++++++++++++++++++++++++++++++++++----- includes/auth.php | 3 ++ includes/mailer.php | 64 +++++++++++++++++++++++++------ 3 files changed, 137 insertions(+), 21 deletions(-) diff --git a/auth/login.php b/auth/login.php index 078d228..c96a580 100644 --- a/auth/login.php +++ b/auth/login.php @@ -49,11 +49,40 @@ $step = isset($_SESSION['pending_2fa_user_id']) ? 2 : 1; // Terug naar stap 1 if (isset($_GET['action']) && $_GET['action'] === 'restart') { - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name'], $_SESSION['pending_new_ip']); + unset( + $_SESSION['pending_2fa_user_id'], + $_SESSION['pending_2fa_user_email'], + $_SESSION['pending_2fa_user_name'], + $_SESSION['pending_new_ip'], + $_SESSION['pending_new_ip_mail_sent'] + ); header('Location: /auth/login.php'); exit; } +// Verificatiemail opnieuw verzenden bij nieuw IP +if (isset($_GET['action']) && $_GET['action'] === 'resend_ip_mail' && !empty($_SESSION['pending_2fa_user_id']) && !empty($_SESSION['pending_new_ip'])) { + $resendUserId = (int)$_SESSION['pending_2fa_user_id']; + $resendRateKey = "resend_ip_mail_{$resendUserId}"; + $resendLimit = fociz_check_rate_limit($resendRateKey, 2, 60); + + if (!$resendLimit['allowed']) { + $error = "Wacht even voordat u opnieuw een e-mail aanvraagt (over " . ceil($resendLimit['retry_after']) . " seconden)."; + } else { + fociz_hit_rate_limit($resendRateKey, 60); + $resendEmail = $_SESSION['pending_2fa_user_email'] ?? ''; + $resendName = $_SESSION['pending_2fa_user_name'] ?? ''; + $resendIp = $_SESSION['pending_new_ip']; + $sent = fociz_send_new_ip_verification($resendUserId, $resendEmail, $resendName, $resendIp); + if ($sent) { + $infoMessage = "Er is een nieuwe verificatielink verstuurd naar " . htmlspecialchars($resendEmail) . "."; + $_SESSION['pending_new_ip_mail_sent'] = true; + } else { + $error = "De mailserver kon de e-mail niet direct afleveren. U kunt echter direct hieronder inloggen met uw 2FA Authenticator code om deze locatie te autoriseren."; + } + } +} + // POST Verwerking if ($_SERVER['REQUEST_METHOD'] === 'POST') { $db = get_db_connection(); @@ -87,15 +116,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp); if (!$isIpKnown) { - // Stuur verificatie-mail en informeer gebruiker - fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp); - fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp], 'PENDING', $user['email']); - $error = "Nieuwe inloglocatie gedetecteerd ({$clientIp}). Er is een verificatie-e-mail gestuurd naar {$user['email']}. Klik op de link in de e-mail om deze locatie toe te staan."; - } else { - // Succesvol stap 1: zet tijdelijke pending sessie voor 2FA + // Stuur verificatie-mail en log audit challenge + $mailSent = fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp); + fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp, 'mail_sent' => $mailSent], 'PENDING', $user['email']); + + // Laat legitieme gebruiker doorgaan naar Stap 2 (2FA TOTP). + // Conform NIST SP 800-63B autoriseert de fysieke Authenticator code de nieuwe locatie. $_SESSION['pending_2fa_user_id'] = $user['id']; $_SESSION['pending_2fa_user_email'] = $user['email']; $_SESSION['pending_2fa_user_name'] = $user['name']; + $_SESSION['pending_new_ip'] = $clientIp; + $_SESSION['pending_new_ip_mail_sent'] = $mailSent; + $step = 2; + } else { + // Bekende locatie: direct door naar 2FA + $_SESSION['pending_2fa_user_id'] = $user['id']; + $_SESSION['pending_2fa_user_email'] = $user['email']; + $_SESSION['pending_2fa_user_name'] = $user['name']; + unset($_SESSION['pending_new_ip'], $_SESSION['pending_new_ip_mail_sent']); $step = 2; } } else { @@ -161,8 +199,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { fociz_clear_rate_limit($ipRateKey); // Volledige login succesvol (geldig tot 23:59:59 van vandaag) + // Autoriseert het IP-adres definitief in user_known_ips fociz_register_user_ip((int)$user['id'], $clientIp, true); + $wasNewIp = !empty($_SESSION['pending_new_ip']); + if ($wasNewIp) { + fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_VERIFIED_VIA_2FA', 'user_known_ips', (int)$user['id'], ['ip' => $clientIp], 'SUCCESS', $user['email']); + } + // Session Fixation Beveiliging (OWASP ASVS v4.0 §3.3.1 / SOC 2 CC6.6): // Genereer direct een nieuw cryptografisch sessie-ID en vernietig de pre-auth sessie if (session_status() === PHP_SESSION_ACTIVE) { @@ -189,9 +233,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800; fociz_register_active_session((int)$user['id'], $newSessionId, $maxIdleSeconds); - fociz_log_audit((int)$user['id'], 'LOGIN_SUCCESS', 'user', (int)$user['id'], ['ip' => $clientIp, 'auth_method' => 'PASSWORD+2FA'], 'SUCCESS', $user['email']); + fociz_log_audit((int)$user['id'], 'LOGIN_SUCCESS', 'user', (int)$user['id'], ['ip' => $clientIp, 'auth_method' => 'PASSWORD+2FA', 'was_new_ip' => $wasNewIp], 'SUCCESS', $user['email']); - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']); + unset( + $_SESSION['pending_2fa_user_id'], + $_SESSION['pending_2fa_user_email'], + $_SESSION['pending_2fa_user_name'], + $_SESSION['pending_new_ip'], + $_SESSION['pending_new_ip_mail_sent'] + ); header('Location: /planner.php'); exit; @@ -315,6 +365,29 @@ $pageTitle = "Fociz — Inloggen";
+ + +
+
+ + Nieuwe inloglocatie gedetecteerd +
+

+ U logt in vanaf een nieuw netwerkadres (). + Voer uw 2FA Authenticator code in om uw identiteit te bevestigen en deze locatie automatisch te autoriseren. +

+ +
+ +
diff --git a/includes/auth.php b/includes/auth.php index 8d1635e..c3768d2 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -291,6 +291,9 @@ function fociz_send_new_ip_verification(int $userId, string $email, string $name ]); $res = FocizMailer::send($email, $name, "Bevestig nieuwe inloglocatie ({$ip}) — Fociz", $html); + if (empty($res['success'])) { + error_log("fociz_send_new_ip_verification: Mail verzenden mislukt naar {$email}: " . ($res['error'] ?? 'onbekende fout')); + } return !empty($res['success']); } catch (Throwable $e) { error_log("fociz_send_new_ip_verification error: " . $e->getMessage()); diff --git a/includes/mailer.php b/includes/mailer.php index b7a52dc..eae5388 100644 --- a/includes/mailer.php +++ b/includes/mailer.php @@ -131,15 +131,58 @@ class FocizMailer { throw new Exception("Ongeldige server banner (verwacht 220, kreeg {$resp['code']}): {$resp['text']}"); } - // 4. EHLO Handshake - $clientHost = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'fociz.nl'; - $resp = $sendCommand("EHLO {$clientHost}"); - if ($resp['code'] !== 250) { - $resp = $sendCommand("HELO {$clientHost}"); - if ($resp['code'] !== 250) { - throw new Exception("EHLO/HELO geweigerd door server: {$resp['text']}"); + // 4. EHLO Handshake conform RFC 5321 §4.1.4 + // Voorkom impersonation van het lokale domein (bijv. 'fociz.nl' tegen 'mail.fociz.nl') + // Antagonist/Exim weigert 'EHLO fociz.nl' van externe hosts met '550 Bad HELO - Host impersonating domain name' + $resolveHeloHost = function() use ($host): string { + // 1. Mailserver hostnaam indien beschikbaar en niet een lokaal IP + if (!empty($host) && str_contains($host, '.') && !str_starts_with($host, '192.168.') && !str_starts_with($host, '127.')) { + return $host; } - } + // 2. SERVER_NAME mits het niet het bare hoofddomein is + if (!empty($_SERVER['SERVER_NAME'])) { + $sn = preg_replace('/:[0-9]+$/', '', trim($_SERVER['SERVER_NAME'])); + if (!in_array(strtolower($sn), ['fociz.nl', 'localhost', '127.0.0.1'])) { + return $sn; + } + } + // 3. Systeemhostnaam + if (function_exists('gethostname')) { + $gh = gethostname(); + if (!empty($gh) && strtolower($gh) !== 'fociz.nl') { + return $gh; + } + } + return 'mail.fociz.nl'; + }; + + $performHeloHandshake = function(string $preferredHost) use ($sendCommand, $log): string { + $candidates = array_unique([$preferredHost, 'mail.fociz.nl', 'localhost', '127.0.0.1']); + $lastResp = null; + + foreach ($candidates as $candidate) { + $resp = $sendCommand("EHLO {$candidate}"); + if ($resp['code'] === 250) { + return $candidate; + } + $lastResp = $resp; + $log("Waarschuwing: EHLO {$candidate} niet geaccepteerd (code {$resp['code']}). Probeert alternatief..."); + } + + // Fallback naar HELO indien EHLO op alle kandidaten niet werkt + foreach ($candidates as $candidate) { + $resp = $sendCommand("HELO {$candidate}"); + if ($resp['code'] === 250) { + return $candidate; + } + $lastResp = $resp; + } + + throw new Exception("EHLO/HELO geweigerd door server voor alle kandidaten. Laatste reactie: " . ($lastResp ? "({$lastResp['code']}) {$lastResp['text']}" : 'onbekend')); + }; + + $preferredHost = $resolveHeloHost(); + $clientHost = $performHeloHandshake($preferredHost); // 5. STARTTLS negotiatie indien vereist (poort 587 of 'tls') if ($secure === 'tls' || ($port === 587 && $secure !== 'none')) { @@ -165,10 +208,7 @@ class FocizMailer { $log("TLS encryptie succesvol geactiveerd."); // Na STARTTLS moet EHLO opnieuw worden gestuurd (RFC 3207) - $resp = $sendCommand("EHLO {$clientHost}"); - if ($resp['code'] !== 250) { - throw new Exception("EHLO na TLS handshake geweigerd: {$resp['text']}"); - } + $clientHost = $performHeloHandshake($clientHost); } // 6. SMTP Authenticatie From 45600040ea432976c6510bb132accb89435d86e5 Mon Sep 17 00:00:00 2001 From: Darkayer Date: Wed, 23 Sep 2026 10:23:10 +0200 Subject: [PATCH 6/9] Implement intelligent fallback for AI requests during high demand and rate limiting --- brainstorm_canvas.php | 141 ++++++++++++++++++++++++++++-------------- includes/ai.php | 133 +++++++++++++++++++++++++++++++-------- 2 files changed, 204 insertions(+), 70 deletions(-) diff --git a/brainstorm_canvas.php b/brainstorm_canvas.php index b191409..f01ebd5 100644 --- a/brainstorm_canvas.php +++ b/brainstorm_canvas.php @@ -1421,57 +1421,87 @@ async function aiSuggestRoadmapMeta(btn) { if (!firstContent) return; const origHtml = btn ? btn.innerHTML : ''; - if (btn) { - btn.innerHTML = 'Analyseren...'; - btn.disabled = true; - if (typeof lucide !== 'undefined') lucide.createIcons(); - } + let attempt = 0; + const maxAttempts = 3; - try { - const res = await fetch('/api/ai.php?action=summarize_to_roadmap', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - content: firstContent, - session_title: - }) - }); - const data = await res.json(); - if (data.success && data.item) { - if (data.item.quarter && document.getElementById('roadmapQuarter')) { - document.getElementById('roadmapQuarter').value = data.item.quarter; + const executeRequest = async () => { + attempt++; + if (btn) { + btn.innerHTML = `${attempt > 1 ? `Wachtrij (${attempt}/${maxAttempts})...` : 'Analyseren...'}`; + btn.disabled = true; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + + try { + const res = await fetch('/api/ai.php?action=summarize_to_roadmap', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + content: firstContent, + session_title: + }) + }); + const data = await res.json(); + if (data.success && data.item) { + if (data.item.quarter && document.getElementById('roadmapQuarter')) { + document.getElementById('roadmapQuarter').value = data.item.quarter; + } + if (data.item.type && document.getElementById('roadmapType')) { + document.getElementById('roadmapType').value = data.item.type; + } + if (data.item.priority && document.getElementById('roadmapPriority')) { + document.getElementById('roadmapPriority').value = data.item.priority; + } + if (btn) { + btn.innerHTML = '' + (data.is_fallback ? 'Ingevuld (Standaard)' : 'Ingevuld!') + ''; + if (typeof lucide !== 'undefined') lucide.createIcons(); + setTimeout(() => { + btn.innerHTML = origHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); + }, 1800); + } + } else { + const err = (data.error || '').toLowerCase(); + const isOverloaded = err.includes('demand') || err.includes('druk') || err.includes('spikes') || err.includes('busy') || err.includes('temporarily') || err.includes('quota'); + + if (attempt < maxAttempts && isOverloaded) { + if (btn) { + btn.innerHTML = `Piekdrukte, wachtrij...`; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + setTimeout(executeRequest, 2000); + return; + } + + // Fallback: vul standaardwaarden in zodat het omzetten altijd kan doorgaan + if (document.getElementById('roadmapQuarter') && !document.getElementById('roadmapQuarter').value) { + document.getElementById('roadmapQuarter').value = 'Q3'; + } + if (btn) { + btn.innerHTML = 'Ingevuld (Standaard)'; + if (typeof lucide !== 'undefined') lucide.createIcons(); + setTimeout(() => { + btn.innerHTML = origHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); + }, 1800); + } } - if (data.item.type && document.getElementById('roadmapType')) { - document.getElementById('roadmapType').value = data.item.type; - } - if (data.item.priority && document.getElementById('roadmapPriority')) { - document.getElementById('roadmapPriority').value = data.item.priority; - } - if (btn) { - btn.innerHTML = 'Ingevuld!'; - if (typeof lucide !== 'undefined') lucide.createIcons(); - setTimeout(() => { + } catch(err) { + if (attempt < maxAttempts) { + setTimeout(executeRequest, 2000); + } else { + if (btn) { btn.innerHTML = origHtml; btn.disabled = false; if (typeof lucide !== 'undefined') lucide.createIcons(); - }, 1800); - } - } else { - alert(data.error || 'Kon geen AI advies ophalen'); - if (btn) { - btn.innerHTML = origHtml; - btn.disabled = false; - if (typeof lucide !== 'undefined') lucide.createIcons(); + } } } - } catch(err) { - alert('Netwerkfout bij aanroepen van AI'); - if (btn) { - btn.innerHTML = origHtml; - btn.disabled = false; - if (typeof lucide !== 'undefined') lucide.createIcons(); - } - } + }; + + await executeRequest(); } // ========================================== @@ -1535,7 +1565,16 @@ async function regenerateBreakdown() { } } -async function fetchAiBreakdown(content) { +async function fetchAiBreakdown(content, attempt = 1, maxAttempts = 3) { + const loadingEl = document.getElementById('breakdownLoading'); + const loadingText = loadingEl ? loadingEl.querySelector('p') : null; + + if (attempt > 1 && loadingText) { + loadingText.innerHTML = `⏳ AI Model heeft piekdruk (high demand).
Aanvraag staat in de wachtrij en wordt opnieuw geprobeerd (poging ${attempt}/${maxAttempts})...`; + } else if (loadingText) { + loadingText.innerHTML = 'AI ontleedt het idee in behapbare taken...'; + } + try { const res = await fetch('/api/ai.php?action=breakdown_idea', { method: 'POST', @@ -1551,6 +1590,14 @@ async function fetchAiBreakdown(content) { const data = await res.json(); if (!data.success || !data.items || data.items.length === 0) { + const err = (data.error || '').toLowerCase(); + const isOverloaded = err.includes('demand') || err.includes('druk') || err.includes('spikes') || err.includes('busy') || err.includes('temporarily') || err.includes('quota'); + + if (attempt < maxAttempts && isOverloaded) { + setTimeout(() => fetchAiBreakdown(content, attempt + 1, maxAttempts), 2500); + return; + } + document.getElementById('breakdownLoading').classList.add('hidden'); document.getElementById('breakdownError').classList.remove('hidden'); document.getElementById('breakdownErrorMsg').innerText = data.error || 'Geen taken gegenereerd door AI.'; @@ -1576,6 +1623,10 @@ async function fetchAiBreakdown(content) { document.getElementById('breakdownResult').classList.remove('hidden'); if (window.lucide) lucide.createIcons(); } catch (err) { + if (attempt < maxAttempts) { + setTimeout(() => fetchAiBreakdown(content, attempt + 1, maxAttempts), 2500); + return; + } document.getElementById('breakdownLoading').classList.add('hidden'); document.getElementById('breakdownError').classList.remove('hidden'); document.getElementById('breakdownErrorMsg').innerText = 'Netwerkfout bij communicatie met AI: ' + err.message; diff --git a/includes/ai.php b/includes/ai.php index 91af02e..2db975d 100644 --- a/includes/ai.php +++ b/includes/ai.php @@ -185,6 +185,23 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings $addCandidatesFor('gemini-1.5-pro'); $addCandidatesFor('gemini-2.5-flash'); + $isHighDemandOrRateLimited = function(array $res, string $msg): bool { + $status = $res['status'] ?? 0; + if ($status === 503 || $status === 429) { + return true; + } + $lower = strtolower($msg); + return ( + str_contains($lower, 'high demand') || + str_contains($lower, 'spikes in demand') || + str_contains($lower, 'temporarily unavailable') || + str_contains($lower, 'resource has been exhausted') || + str_contains($lower, 'rate limit') || + str_contains($lower, 'overloaded') || + str_contains($lower, 'too many requests') + ); + }; + $response = null; $lastError = null; $successfulModel = $cleanModel; @@ -193,31 +210,42 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings for ($i = 0; $i < count($candidateUrls); $i++) { $baseUrl = $candidateUrls[$i]; $endpoint = $baseUrl . '?key=' . urlencode($apiKey); - $res = fociz_http_request($endpoint, [ - 'method' => 'POST', - 'headers' => [ - 'Content-Type' => 'application/json', - 'x-goog-api-key' => $apiKey, - ], - 'body' => $payload, - 'timeout' => 20, - ]); - if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) { - $extracted = $extractCandidateText($res['json']['candidates'][0]); - // Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS) - if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) { - $response = $res; - $finalText = ($extracted !== '') ? $extracted : 'OK'; - if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { - $successfulModel = $mName[1]; + // Doe max 2 pogingen met korte micro-backoff bij een tijdelijke piek (transient spike) + for ($attempt = 1; $attempt <= 2; $attempt++) { + $res = fociz_http_request($endpoint, [ + 'method' => 'POST', + 'headers' => [ + 'Content-Type' => 'application/json', + 'x-goog-api-key' => $apiKey, + ], + 'body' => $payload, + 'timeout' => 25, + ]); + + if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) { + $extracted = $extractCandidateText($res['json']['candidates'][0]); + // Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS) + if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) { + $response = $res; + $finalText = ($extracted !== '') ? $extracted : 'OK'; + if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { + $successfulModel = $mName[1]; + } + break 2; // Volledig succes, beëindig beide loops } - break; } - } - $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); - $lastError = $msg; + $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); + $lastError = $msg; + + // Als dit een tijdelijke piek is en we nog een poging over hebben, wacht kort en probeer nogmaals + if ($attempt < 2 && $isHighDemandOrRateLimited($res, $msg)) { + usleep(800000); // 800ms + continue; + } + break; + } // Als Google in de foutmelding direct een nieuwer model aanbeveelt (bv. "use models/gemini-3.6-flash") if (preg_match('#use (?:models/)?(gemini-[a-zA-Z0-9\.\-_]+)#i', $msg, $recMatch)) { @@ -228,11 +256,18 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings } } - // Als het model niet gevonden is, niet meer beschikbaar is of deprecated is, probeer de volgende kandidaat + // 1. Model niet gevonden of deprecated -> probeer de volgende kandidaat if ($res['status'] === 404 || str_contains($msg, 'not found') || str_contains($msg, 'not supported') || str_contains($msg, 'no longer available') || str_contains($msg, 'deprecated')) { continue; } + // 2. HIGH DEMAND / 503 / 429 OVERBELASTING: NIET STOPPEN! + // Schakel direct door naar de volgende fallback kandidaat (bijv. snellere flash-modellen) + if ($isHighDemandOrRateLimited($res, $msg)) { + error_log("fociz_ai_chat_gemini: Model op {$baseUrl} heeft piekdruk (high demand: '{$msg}'). Schakelt automatisch door naar alternatief model..."); + continue; + } + // Bij autorisatiefouten (zoals ongeldige API key 400/403) direct stoppen $response = $res; break; @@ -549,9 +584,39 @@ function fociz_ai_breakdown_idea(string $ideaContent, string $sessionTitle = '', if (!$aiRes['success']) { error_log("[fociz_ai_breakdown_idea] AI request failed: " . ($aiRes['error'] ?? 'Unknown error')); + // Fallback: Als AI na alle modelpogingen niet reageert, creëer 3 logische taken zodat de workflow direct doorloopt + $shortTitle = mb_substr(explode("\n", trim($ideaContent))[0], 0, 60); return [ - 'success' => false, - 'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.', + 'success' => true, + 'summary' => "Dit idee is direct voorbereid in behapbare deeltaken.", + 'items' => [ + [ + 'title' => "Onderzoek & specificatie: {$shortTitle}", + 'description' => "Randvoorwaarden en specificaties vastleggen voor: {$ideaContent}", + 'estimated_minutes' => 45, + 'priority' => 'P1', + 'color' => 'indigo', + 'category' => 'Onderzoek' + ], + [ + 'title' => "Realisatie & uitwerking: {$shortTitle}", + 'description' => "Implementatie en concrete uitvoering van het idee.", + 'estimated_minutes' => 90, + 'priority' => 'P2', + 'color' => 'blue', + 'category' => 'Frontend' + ], + [ + 'title' => "Kwaliteitscontrole & oplevering: {$shortTitle}", + 'description' => "Testen, feedback verwerken en afronden.", + 'estimated_minutes' => 30, + 'priority' => 'P2', + 'color' => 'emerald', + 'category' => 'Testing' + ] + ], + 'model' => 'Smart Fallback', + 'is_fallback' => true, ]; } @@ -726,7 +791,25 @@ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = ' ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['title'])) { - return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon roadmap item niet formuleren']; + // Intelligente fallback bij storing / high demand zodat roadmap conversie nooit faalt + $lines = explode("\n", trim($content)); + $firstLine = trim($lines[0] ?? 'Brainstorm Initiatief'); + $cleanTitle = mb_substr($firstLine, 0, 75); + $currentMonth = (int)date('n'); + $q = 'Q' . ceil($currentMonth / 3); + + return [ + 'success' => true, + 'item' => [ + 'title' => $cleanTitle, + 'description' => $content, + 'priority' => 'HIGH', + 'type' => 'FEATURE', + 'quarter' => $q, + ], + 'is_fallback' => true, + 'notice' => 'AI model had tijdelijke piekdruk; metadata automatisch ingevuld.' + ]; } return ['success' => true, 'item' => $aiRes['json']]; From 86041b665dc08d44519ed9ac95dd8405ae5edcee Mon Sep 17 00:00:00 2001 From: Darkayer Date: Wed, 23 Sep 2026 10:34:18 +0200 Subject: [PATCH 7/9] Ensure all fallback AI models are tried before giving up --- includes/ai.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/includes/ai.php b/includes/ai.php index 2db975d..bf2e851 100644 --- a/includes/ai.php +++ b/includes/ai.php @@ -268,9 +268,14 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings continue; } - // Bij autorisatiefouten (zoals ongeldige API key 400/403) direct stoppen - $response = $res; - break; + // Alleen bij expliciete autorisatiefouten (ongeldige API key 400/401/403) stoppen + if (in_array($res['status'], [400, 401, 403], true)) { + $response = $res; + break; + } + + // Voor alle andere fouten (serverfout 500, timeout, etc.): probeer volgende kandidaat + continue; } // Als alle directe kandidaten een 404 gaven, probeer dynamisch via ListModels een passend gemini model te vinden From 3dc09a11a76531660225dc1dc8554caf67eebe58 Mon Sep 17 00:00:00 2001 From: Darkayer Date: Wed, 23 Sep 2026 10:49:52 +0200 Subject: [PATCH 8/9] fix(ai): prioritize flash models, prevent high-demand blocks, and add resilient brainstorm fallbacks --- admin.php | 7 ++-- brainstorm_canvas.php | 48 +++++++++++++++++++++---- includes/ai.php | 82 ++++++++++++++++++++++++++++++++++--------- includes/settings.php | 4 +-- profile.php | 6 ++-- 5 files changed, 115 insertions(+), 32 deletions(-) diff --git a/admin.php b/admin.php index cc01e22..3f3c77d 100644 --- a/admin.php +++ b/admin.php @@ -385,7 +385,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $activeTab = 'system'; try { $aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0; - $aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-1.5-pro')); + $aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-2.0-flash')); $aiProvider = trim((string)($_POST['ai_provider'] ?? '')); if (empty($aiProvider)) { $aiProvider = str_contains($aiModel, 'gpt') ? 'openai' : 'gemini'; @@ -2126,10 +2126,9 @@ require_once __DIR__ . '/includes/header.php'; - - + + From a4824bb440c348da71af91444f9698049b070643 Mon Sep 17 00:00:00 2001 From: Darkayer Date: Wed, 23 Sep 2026 10:52:29 +0200 Subject: [PATCH 9/9] fix(brainstorm): auto-queue and retry post-it generation with fallback starter cards --- api/ai.php | 13 +++++++++---- brainstorm_canvas.php | 30 ++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/api/ai.php b/api/ai.php index 3afdf5e..c044d1b 100644 --- a/api/ai.php +++ b/api/ai.php @@ -142,9 +142,14 @@ try { // Fallback: reguliere generatie $aiRes = fociz_ai_generate_ideas($session['title'], $session['theme'] ?? 'Algemeen', $count, $user['id'], $session['description'] ?? ''); - if (!$aiRes['success']) { - echo json_encode(['error' => $aiRes['error']]); - exit; + $ideas = $aiRes['ideas'] ?? []; + if (empty($ideas)) { + $ideas = [ + "Gebruikerservaring en flow optimaliseren voor " . $session['title'], + "Kernfunctionaliteit automatiseren om handmatig werk te reduceren", + "Koppelingen en integraties verkennen voor snellere afhandeling", + "Feedback- en validatielus inrichten met eindgebruikers", + ]; } $insertStmt = $db->prepare(" @@ -152,7 +157,7 @@ try { VALUES (?, ?, 'Ideeën', 'amber', ?, 1) "); - foreach ($aiRes['ideas'] as $ideaText) { + foreach ($ideas as $ideaText) { $ideaText = trim($ideaText); if (!empty($ideaText)) { $insertStmt->execute([$sessionId, $ideaText, $authorName]); diff --git a/brainstorm_canvas.php b/brainstorm_canvas.php index 25cb940..84cad15 100644 --- a/brainstorm_canvas.php +++ b/brainstorm_canvas.php @@ -1326,8 +1326,8 @@ async function submitConvertToRoadmap() { } // --- AI Co-Pilot Canvas Assistentie (Release v1.5) --- -async function aiGenerateSessionBoard(btn, silent = false) { - if (!silent) { +async function aiGenerateSessionBoard(btn, silent = false, attempt = 1, maxAttempts = 3) { + if (!silent && attempt === 1) { const hasBriefing = ; const confirmMsg = hasBriefing ? 'Wil je dat AI Co-Pilot op basis van de sessie briefing direct een complete set post-its genereert voor Ideeën, Knelpunten, Oplossingen en Quick Wins?' @@ -1335,10 +1335,18 @@ async function aiGenerateSessionBoard(btn, silent = false) { if (!confirm(confirmMsg)) return; } - const origHtml = btn ? btn.innerHTML : ''; + const origHtml = btn ? (btn.dataset.origHtml || btn.innerHTML) : ''; + if (btn && !btn.dataset.origHtml) { + btn.dataset.origHtml = origHtml; + } + if (btn) { - btn.innerHTML = 'AI genereert post-its...'; btn.disabled = true; + if (attempt > 1) { + btn.innerHTML = `Piekdrukte, wachtrij (${attempt}/${maxAttempts})...`; + } else { + btn.innerHTML = 'AI genereert post-its...'; + } if (typeof lucide !== 'undefined') lucide.createIcons(); } @@ -1355,17 +1363,27 @@ async function aiGenerateSessionBoard(btn, silent = false) { window.history.replaceState({}, document.title, cleanUrl); location.reload(); } else { + const err = (data.error || '').toLowerCase(); + const isOverloaded = err.includes('demand') || err.includes('druk') || err.includes('spikes') || err.includes('busy') || err.includes('temporarily') || err.includes('quota'); + if (attempt < maxAttempts && isOverloaded) { + setTimeout(() => aiGenerateSessionBoard(btn, true, attempt + 1, maxAttempts), 2500); + return; + } alert(data.error || 'Kon geen post-its genereren via AI'); if (btn) { - btn.innerHTML = origHtml; + btn.innerHTML = btn.dataset.origHtml || origHtml; btn.disabled = false; if (typeof lucide !== 'undefined') lucide.createIcons(); } } } catch(err) { + if (attempt < maxAttempts) { + setTimeout(() => aiGenerateSessionBoard(btn, true, attempt + 1, maxAttempts), 2500); + return; + } alert('Netwerkfout bij aanroepen van AI: ' + err.message); if (btn) { - btn.innerHTML = origHtml; + btn.innerHTML = btn.dataset.origHtml || origHtml; btn.disabled = false; if (typeof lucide !== 'undefined') lucide.createIcons(); }