Compare commits

...

2 Commits

Author SHA1 Message Date
NiekRabelink 26076f92f8 Merge pull request 'feat(cache): implement centralized caching layer for improved performance and reduced API calls' (#39) from dev into main
Reviewed-on: #39
2026-09-10 13:20:56 +00:00
NiekRabelink 72ad1fb70e feat(cache): implement centralized caching layer for improved performance and reduced API calls
feat(microsoft-integration): enhance inbox and events fetching with caching support
feat(auth): include theme and notification settings in user data retrieval
feat(profile): improve password visibility toggle functionality
feat(planner): optimize event synchronization with Outlook using caching
fix(notifications): update mail badge handling for mobile and desktop
2026-09-10 15:19:46 +02:00
15 changed files with 334 additions and 85 deletions
+9 -7
View File
@@ -471,8 +471,8 @@ require_once __DIR__ . '/includes/header.php';
<span class="text-[10px] text-slate-500">Wordt veilig en versleuteld opgeslagen</span> <span class="text-[10px] text-slate-500">Wordt veilig en versleuteld opgeslagen</span>
</div> </div>
<div class="relative"> <div class="relative">
<input type="password" id="adminClientSecret" name="client_secret" value="<?= !empty($appConfig['client_secret']) ? '••••••••••••••••' : '' ?>" placeholder="Voer de Secret Value in uit Azure..." class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"> <input type="password" id="adminClientSecret" name="client_secret" value="<?= htmlspecialchars($appConfig['client_secret'] ?? '') ?>" placeholder="Voer de Secret Value in uit Azure..." class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500">
<button type="button" onclick="toggleAdminSecret()" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"> <button type="button" onclick="toggleAdminSecret()" id="toggleSecretBtn" title="Toon / verberg secret" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer">
<i data-lucide="eye" class="w-4 h-4" id="eyeSecretIcon"></i> <i data-lucide="eye" class="w-4 h-4" id="eyeSecretIcon"></i>
</button> </button>
</div> </div>
@@ -1204,16 +1204,18 @@ function copyAdminRedirectUri() {
function toggleAdminSecret() { function toggleAdminSecret() {
const input = document.getElementById('adminClientSecret'); const input = document.getElementById('adminClientSecret');
const icon = document.getElementById('eyeSecretIcon'); const btn = document.getElementById('toggleSecretBtn');
if (!input || !icon) return; if (!input) return;
if (input.type === 'password') { if (input.type === 'password') {
input.type = 'text'; input.type = 'text';
icon.setAttribute('data-lucide', 'eye-off'); if (btn) btn.innerHTML = '<i data-lucide="eye-off" class="w-4 h-4"></i>';
} else { } else {
input.type = 'password'; input.type = 'password';
icon.setAttribute('data-lucide', 'eye'); if (btn) btn.innerHTML = '<i data-lucide="eye" class="w-4 h-4"></i>';
}
if (window.lucide && typeof lucide.createIcons === 'function') {
lucide.createIcons();
} }
if (window.lucide) lucide.createIcons();
} }
function toggleAdminGuide() { function toggleAdminGuide() {
+5
View File
@@ -12,6 +12,11 @@ $user = require_auth();
$userId = (int)$user['id']; $userId = (int)$user['id'];
$db = get_db_connection(); $db = get_db_connection();
// Geef het PHP sessieslot direct vrij zodat gelijktijdige paginanavigatie nooit geblokkeerd wordt
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
try { try {
+7 -1
View File
@@ -11,6 +11,11 @@ $user = require_auth();
$userId = (int)$user['id']; $userId = (int)$user['id'];
$db = get_db_connection(); $db = get_db_connection();
// Geef het PHP sessieslot direct vrij zodat paginanavigatie nooit geblokkeerd wordt
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? ($_POST['action'] ?? ''); $action = $_GET['action'] ?? ($_POST['action'] ?? '');
@@ -20,8 +25,9 @@ try {
if ($action === 'get_inbox') { if ($action === 'get_inbox') {
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25;
$unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1'; $unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1';
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly); $res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly, $forceRefresh);
echo json_encode($res); echo json_encode($res);
exit; exit;
} }
+5
View File
@@ -9,6 +9,11 @@ require_once __DIR__ . '/../includes/integrations/microsoft.php';
$user = require_auth(); $user = require_auth();
$db = get_db_connection(); $db = get_db_connection();
// Geef het PHP sessieslot direct vrij zodat paginanavigatie nooit geblokkeerd wordt
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? ($_POST['action'] ?? ''); $action = $_GET['action'] ?? ($_POST['action'] ?? '');
+25 -12
View File
@@ -425,15 +425,21 @@
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
if (data && data.success) { if (data && data.success) {
// Update mail badge in navigatie // Update mail badge in navigatie (desktop & mobiel)
const mailBadge = document.getElementById('navMailBadge'); const mailBadge = document.getElementById('navMailBadge');
if (mailBadge && typeof data.mail?.unread_count !== 'undefined') { const mailBadgeMobile = document.getElementById('navMailBadgeMobile');
if (data.mail.unread_count > 0) { if (typeof data.mail?.unread_count !== 'undefined') {
mailBadge.innerText = data.mail.unread_count; const count = parseInt(data.mail.unread_count, 10) || 0;
mailBadge.classList.remove('hidden'); const badgeText = count > 99 ? '99+' : count;
[mailBadge, mailBadgeMobile].forEach(el => {
if (!el) return;
if (count > 0) {
el.innerText = badgeText;
el.classList.remove('hidden');
} else { } else {
mailBadge.classList.add('hidden'); el.classList.add('hidden');
} }
});
} }
// E-mail melding // E-mail melding
@@ -500,18 +506,25 @@
setInterval(refreshEvents, 60000); setInterval(refreshEvents, 60000);
// Evaluatie interval elke 15 seconden voor agenda popups // Evaluatie interval elke 15 seconden voor agenda popups
setInterval(evaluateEvents, 15000); setInterval(evaluateEvents, 15000);
// Live sync interval elke 12 seconden voor instant mail, Jira & calendar updates // Live sync interval elke 30 seconden voor mail, Jira & calendar updates
setInterval(checkLiveSync, 12000); setInterval(checkLiveSync, 30000);
window.addEventListener('focus', () => { // Intelligente throttle voor window focus en tabwisseling (max 1x per 20s)
let lastFocusCheck = Date.now();
const throttledSync = () => {
const now = Date.now();
if (now - lastFocusCheck > 20000) {
lastFocusCheck = now;
checkLiveSync(); checkLiveSync();
refreshEvents(); refreshEvents();
}); }
};
window.addEventListener('focus', throttledSync);
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
checkLiveSync(); throttledSync();
refreshEvents();
} }
}); });
+7
View File
@@ -235,3 +235,10 @@ CREATE TABLE IF NOT EXISTS `jira_queue_filters` (
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `app_cache` (
`cache_key` VARCHAR(128) NOT NULL PRIMARY KEY,
`cache_value` MEDIUMTEXT NOT NULL,
`expires_at` INT UNSIGNED NOT NULL,
INDEX `idx_app_cache_expires` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+7 -4
View File
@@ -211,7 +211,7 @@ function get_logged_in_user(): ?array {
// 2. Controleer actieve PHP sessie (mits behorend bij vandaag) // 2. Controleer actieve PHP sessie (mits behorend bij vandaag)
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) { if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, created_at FROM users WHERE id = ?"); $stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]); $stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch(); $user = $stmt->fetch();
if ($user) { if ($user) {
@@ -229,7 +229,7 @@ function get_logged_in_user(): ?array {
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart of server idle timeout (uitsluitend vandaag!) // 3. Fallback dagsessie-token: herstelt sessie na browserherstart of server idle timeout (uitsluitend vandaag!)
$dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null); $dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null);
if (!empty($dayToken) && is_string($dayToken)) { if (!empty($dayToken) && is_string($dayToken)) {
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?"); $stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
$stmt->execute([$dayToken, $today]); $stmt->execute([$dayToken, $today]);
$user = $stmt->fetch(); $user = $stmt->fetch();
if ($user) { if ($user) {
@@ -471,10 +471,13 @@ function update_user_notification_settings(int $userId, array $settings): bool {
* Haalt het voorkeursthema op voor een gebruiker ('dark', 'light' of 'system') * Haalt het voorkeursthema op voor een gebruiker ('dark', 'light' of 'system')
*/ */
function get_user_theme(int $userId): string { function get_user_theme(int $userId): string {
$db = get_db_connection(); global $currentUser;
ensure_theme_column($db); if (!empty($currentUser['id']) && (int)$currentUser['id'] === $userId && !empty($currentUser['theme'])) {
return $currentUser['theme'];
}
try { try {
$db = get_db_connection();
$stmt = $db->prepare("SELECT theme FROM users WHERE id = ?"); $stmt = $db->prepare("SELECT theme FROM users WHERE id = ?");
$stmt->execute([$userId]); $stmt->execute([$userId]);
$theme = $stmt->fetchColumn(); $theme = $stmt->fetchColumn();
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* Centrale Caching Laag voor Fociz
* Biedt een supersnelle database- én in-memory transiënte cache voor externe API-aanroepen (Microsoft Graph, Jira Cloud).
*/
require_once __DIR__ . '/../config/database.php';
function ensure_app_cache_table(?PDO $db = null): void {
static $ensured = false;
if ($ensured) return;
try {
$pdo = $db ?: get_db_connection();
$pdo->exec("
CREATE TABLE IF NOT EXISTS `app_cache` (
`cache_key` VARCHAR(128) NOT NULL PRIMARY KEY,
`cache_value` MEDIUMTEXT NOT NULL,
`expires_at` INT UNSIGNED NOT NULL,
INDEX `idx_app_cache_expires` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
$ensured = true;
} catch (Throwable $e) {
error_log("ensure_app_cache_table error: " . $e->getMessage());
}
}
/**
* Haalt een item op uit de cache.
* Controleert eerst het runtime statisch geheugen, vervolgens MariaDB.
*/
function fociz_cache_get(string $key, $default = null) {
static $memoryCache = [];
$now = time();
// 1. Runtime memory check (0ms)
if (isset($memoryCache[$key])) {
if ($memoryCache[$key]['expires_at'] > $now) {
return $memoryCache[$key]['value'];
}
unset($memoryCache[$key]);
}
// 2. Database check (< 0.5ms)
try {
$db = get_db_connection();
ensure_app_cache_table($db);
$stmt = $db->prepare("SELECT cache_value, expires_at FROM app_cache WHERE cache_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch();
if ($row) {
if ((int)$row['expires_at'] > $now) {
$decoded = json_decode($row['cache_value'], true);
$val = ($decoded !== null || $row['cache_value'] === 'null') ? $decoded : $row['cache_value'];
$memoryCache[$key] = [
'value' => $val,
'expires_at' => (int)$row['expires_at']
];
return $val;
} else {
// Verlopen item direct opruimen
$del = $db->prepare("DELETE FROM app_cache WHERE cache_key = ?");
$del->execute([$key]);
}
}
} catch (Throwable $e) {
error_log("fociz_cache_get error for {$key}: " . $e->getMessage());
}
return $default;
}
/**
* Slaat een item op in de cache met een bepaalde levensduur in seconden.
*/
function fociz_cache_set(string $key, $value, int $ttlSeconds = 60): bool {
static $memoryCache = [];
$now = time();
$expiresAt = $now + max(1, $ttlSeconds);
// 1. Sla op in runtime memory
$memoryCache[$key] = [
'value' => $value,
'expires_at' => $expiresAt
];
// 2. Sla op in database
try {
$db = get_db_connection();
ensure_app_cache_table($db);
$valToStore = is_string($value) ? $value : json_encode($value, JSON_UNESCAPED_UNICODE);
$stmt = $db->prepare("
INSERT INTO app_cache (cache_key, cache_value, expires_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
cache_value = VALUES(cache_value),
expires_at = VALUES(expires_at)
");
return $stmt->execute([$key, $valToStore, $expiresAt]);
} catch (Throwable $e) {
error_log("fociz_cache_set error for {$key}: " . $e->getMessage());
return false;
}
}
/**
* Verwijdert een specifiek item uit de cache.
*/
function fociz_cache_delete(string $key): bool {
try {
$db = get_db_connection();
ensure_app_cache_table($db);
$stmt = $db->prepare("DELETE FROM app_cache WHERE cache_key = ?");
return $stmt->execute([$key]);
} catch (Throwable $e) {
return false;
}
}
/**
* Verwijdert alle items met een bepaald voorvoegsel (bijv. 'ms_events_1_')
*/
function fociz_cache_delete_pattern(string $prefix): bool {
try {
$db = get_db_connection();
ensure_app_cache_table($db);
$stmt = $db->prepare("DELETE FROM app_cache WHERE cache_key LIKE ?");
return $stmt->execute([$prefix . '%']);
} catch (Throwable $e) {
return false;
}
}
+6 -3
View File
@@ -165,10 +165,10 @@ require_once __DIR__ . '/env_badge.php';
<span>Fociz</span> <span>Fociz</span>
</a> </a>
<a href="/mail.php" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition relative <?= $currentPage === 'mail.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>"> <a href="/mail.php" class="relative flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold transition <?= $currentPage === 'mail.php' ? 'bg-indigo-50 text-indigo-700 border border-indigo-200/70 shadow-2xs dark:bg-indigo-500/10 dark:text-indigo-300 dark:border-indigo-500/20' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800/60' ?>">
<i data-lucide="mail" class="w-4 h-4 text-sky-500"></i> <i data-lucide="mail" class="w-4 h-4 text-sky-500"></i>
<span>Mail</span> <span>Mail</span>
<span id="navMailBadge" class="hidden px-1.5 py-0.2 rounded-full text-[10px] font-black bg-sky-600 text-white leading-none"></span> <span id="navMailBadge" class="hidden absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-black bg-sky-500 text-white flex items-center justify-center leading-none ring-2 ring-white dark:ring-[#0c0f17] shadow-xs pointer-events-none"></span>
</a> </a>
<!-- Dropdown: Projecten (Bord, Tijdlijn, Brainstorm, Roadmap) --> <!-- Dropdown: Projecten (Bord, Tijdlijn, Brainstorm, Roadmap) -->
@@ -284,8 +284,11 @@ require_once __DIR__ . '/env_badge.php';
<i data-lucide="layout-dashboard" class="w-4 h-4 mb-0.5"></i> <i data-lucide="layout-dashboard" class="w-4 h-4 mb-0.5"></i>
<span>Fociz</span> <span>Fociz</span>
</a> </a>
<a href="/mail.php" class="flex flex-col items-center py-1 <?= $currentPage === 'mail.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>"> <a href="/mail.php" class="relative flex flex-col items-center py-1 <?= $currentPage === 'mail.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
<div class="relative">
<i data-lucide="mail" class="w-4 h-4 mb-0.5"></i> <i data-lucide="mail" class="w-4 h-4 mb-0.5"></i>
<span id="navMailBadgeMobile" class="hidden absolute -top-1 -right-2 min-w-[14px] h-[14px] px-0.5 rounded-full text-[8px] font-black bg-sky-500 text-white flex items-center justify-center leading-none ring-2 ring-white dark:ring-[#0c0f17]"></span>
</div>
<span>Mail</span> <span>Mail</span>
</a> </a>
<a href="/board.php" class="flex flex-col items-center py-1 <?= $currentPage === 'board.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>"> <a href="/board.php" class="flex flex-col items-center py-1 <?= $currentPage === 'board.php' ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
+17 -8
View File
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../http.php'; require_once __DIR__ . '/../http.php';
require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../cache.php';
/** /**
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde * Normaliseert het Jira domein naar een schone URL zonder slash aan het einde
@@ -823,15 +824,18 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
]; ];
} }
// Sessiecache voor 60 seconden // Cache controle (60 seconden)
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
$cacheKey = "jira_dashboard_user_{$userId}"; $cacheKey = "jira_dashboard_user_{$userId}";
$cached = $_SESSION[$cacheKey] ?? null; if (!$forceRefresh) {
$cachedData = fociz_cache_get($cacheKey);
if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 60)) { if ($cachedData !== null && is_array($cachedData)) {
return array_merge($cached['data'], ['from_cache' => true]); return array_merge($cachedData, ['from_cache' => true]);
}
if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$cacheKey]['data'])) {
if (($_SESSION[$cacheKey]['timestamp'] ?? 0) > (time() - 60)) {
return array_merge($_SESSION[$cacheKey]['data'], ['from_cache' => true]);
}
}
} }
// Ophalen van toegewezen taken // Ophalen van toegewezen taken
@@ -865,10 +869,14 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'default_queue_filter' => $userQueuePref, 'default_queue_filter' => $userQueuePref,
]; ];
fociz_cache_set($cacheKey, $data, 60);
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION[$cacheKey] = [ $_SESSION[$cacheKey] = [
'timestamp' => time(), 'timestamp' => time(),
'data' => $data, 'data' => $data,
]; ];
}
// Werk last_sync_at bij in database // Werk last_sync_at bij in database
$stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?"); $stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?");
@@ -902,6 +910,7 @@ function jira_disconnect(int $userId): bool {
} }
unset($_SESSION["jira_issues_user_{$userId}"]); unset($_SESSION["jira_issues_user_{$userId}"]);
unset($_SESSION["jira_dashboard_user_{$userId}"]); unset($_SESSION["jira_dashboard_user_{$userId}"]);
fociz_cache_delete("jira_dashboard_user_{$userId}");
return $stmt->execute([$userId]); return $stmt->execute([$userId]);
} }
+40 -6
View File
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../http.php'; require_once __DIR__ . '/../http.php';
require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../cache.php';
const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize'; const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize';
const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token'; const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
@@ -279,7 +280,15 @@ function microsoft_get_valid_token(int $userId, bool $forceRefresh = false): ?st
/** /**
* Haalt agenda-afspraken van Microsoft Outlook op voor een datumbereik (bijv. een hele week) * Haalt agenda-afspraken van Microsoft Outlook op voor een datumbereik (bijv. een hele week)
*/ */
function microsoft_fetch_events_range(int $userId, string $startDate, string $endDate): array { function microsoft_fetch_events_range(int $userId, string $startDate, string $endDate, bool $forceRefresh = false): array {
$cacheKey = "ms_events_{$userId}_{$startDate}_{$endDate}";
if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) {
return $cached;
}
}
$token = microsoft_get_valid_token($userId); $token = microsoft_get_valid_token($userId);
if (!$token) { if (!$token) {
return []; return [];
@@ -400,6 +409,7 @@ function microsoft_fetch_events_range(int $userId, string $startDate, string $en
]; ];
} }
fociz_cache_set($cacheKey, $events, 60);
return $events; return $events;
} }
@@ -520,6 +530,7 @@ function microsoft_create_event(int $userId, array $eventData): array {
} }
if ($response['status'] === 201 && is_array($response['json'])) { if ($response['status'] === 201 && is_array($response['json'])) {
fociz_cache_delete_pattern("ms_events_{$userId}_");
return [ return [
'success' => true, 'success' => true,
'event_id' => $response['json']['id'] ?? null, 'event_id' => $response['json']['id'] ?? null,
@@ -559,7 +570,11 @@ function microsoft_delete_event(int $userId, string $eventId): bool {
'timeout' => 12, 'timeout' => 12,
]); ]);
return in_array($response['status'], [200, 204, 404]); $ok = in_array($response['status'], [200, 204, 404]);
if ($ok) {
fociz_cache_delete_pattern("ms_events_{$userId}_");
}
return $ok;
} }
/** /**
@@ -628,7 +643,11 @@ function microsoft_update_event(int $userId, string $eventId, array $eventData):
} }
} }
return in_array($response['status'], [200, 204]); $ok = in_array($response['status'], [200, 204]);
if ($ok) {
fociz_cache_delete_pattern("ms_events_{$userId}_");
}
return $ok;
} }
/** /**
@@ -985,7 +1004,15 @@ function microsoft_get_mail_demo_data(): array {
/** /**
* Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data) * Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
*/ */
function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unreadOnly = false): array { function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unreadOnly = false, bool $forceRefresh = false): array {
$cacheKey = "ms_inbox_{$userId}_{$limit}_" . ($unreadOnly ? '1' : '0');
if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) {
return $cached;
}
}
$db = get_db_connection(); $db = get_db_connection();
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); $stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmt->execute([$userId]); $stmt->execute([$userId]);
@@ -1118,13 +1145,15 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$unreadCount = count(array_filter($messages, function($m) { return empty($m['is_read']); })); $unreadCount = count(array_filter($messages, function($m) { return empty($m['is_read']); }));
return [ $result = [
'success' => true, 'success' => true,
'is_demo' => false, 'is_demo' => false,
'messages' => $messages, 'messages' => $messages,
'unread_count' => $unreadCount, 'unread_count' => $unreadCount,
'total_count' => count($messages), 'total_count' => count($messages),
]; ];
fociz_cache_set($cacheKey, $result, 45); // 45 seconden cache
return $result;
} }
/** /**
@@ -1239,7 +1268,11 @@ function microsoft_mark_message_read(int $userId, string $messageId, bool $isRea
'timeout' => 10, 'timeout' => 10,
]); ]);
return ($res['status'] >= 200 && $res['status'] < 300); $ok = ($res['status'] >= 200 && $res['status'] < 300);
if ($ok) {
fociz_cache_delete_pattern("ms_inbox_{$userId}_");
}
return $ok;
} }
/** /**
@@ -1271,6 +1304,7 @@ function microsoft_reply_message(int $userId, string $messageId, string $comment
]); ]);
if ($res['status'] >= 200 && $res['status'] < 300) { if ($res['status'] >= 200 && $res['status'] < 300) {
fociz_cache_delete_pattern("ms_inbox_{$userId}_");
return ['success' => true, 'message' => 'Antwoord succesvol verzonden via Outlook.']; return ['success' => true, 'message' => 'Antwoord succesvol verzonden via Outlook.'];
} }
+30 -20
View File
@@ -29,36 +29,42 @@ function ensure_system_settings_table(?PDO $db = null): void {
} }
/** /**
* Haalt een enkele instelling op uit MariaDB * Haalt alle systeeminstellingen in 1 geoptimaliseerde query op en bewaart deze in het static runtime geheugen
*/ */
function get_system_setting(string $key, $default = null) { function get_all_system_settings(bool $forceRefresh = false): array {
static $settingsCache = null;
if ($settingsCache !== null && !$forceRefresh) {
return $settingsCache;
}
$settingsCache = [];
try { try {
$db = get_db_connection(); $db = get_db_connection();
ensure_system_settings_table($db); ensure_system_settings_table($db);
$stmt = $db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?"); $stmt = $db->query("SELECT setting_key, setting_value FROM system_settings");
$stmt->execute([$key]); while ($row = $stmt->fetch()) {
$val = $stmt->fetchColumn(); $val = $row['setting_value'];
if ($val === false || $val === null) {
return $default;
}
// Decodeer JSON indien van toepassing
$decoded = json_decode($val, true); $decoded = json_decode($val, true);
if (json_last_error() === JSON_ERROR_NONE) { $settingsCache[$row['setting_key']] = (json_last_error() === JSON_ERROR_NONE) ? $decoded : $val;
return $decoded; }
} catch (Throwable $e) {
error_log("get_all_system_settings error: " . $e->getMessage());
} }
return $val; return $settingsCache;
} catch (Throwable $e) {
error_log("get_system_setting error for {$key}: " . $e->getMessage());
return $default;
}
} }
/** /**
* Slaat een instelling op in MariaDB * Haalt een enkele instelling op uit het geheugen (of fallback default)
*/
function get_system_setting(string $key, $default = null) {
$all = get_all_system_settings();
return array_key_exists($key, $all) ? $all[$key] : $default;
}
/**
* Slaat een instelling op in MariaDB en ververst direct het cachegeheugen
*/ */
function set_system_setting(string $key, $value): bool { function set_system_setting(string $key, $value): bool {
try { try {
@@ -72,7 +78,11 @@ function set_system_setting(string $key, $value): bool {
VALUES (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = NOW() ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = NOW()
"); ");
return $stmt->execute([$key, $valToStore]); $ok = $stmt->execute([$key, $valToStore]);
if ($ok) {
get_all_system_settings(true);
}
return $ok;
} catch (Throwable $e) { } catch (Throwable $e) {
error_log("set_system_setting error for {$key}: " . $e->getMessage()); error_log("set_system_setting error for {$key}: " . $e->getMessage());
return false; return false;
+4 -3
View File
@@ -11,8 +11,9 @@ require_once __DIR__ . '/includes/integrations/microsoft.php';
$userId = (int)($currentUser['id'] ?? 0); $userId = (int)($currentUser['id'] ?? 0);
$db = get_db_connection(); $db = get_db_connection();
// Haal initiële inbox data op // Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
$inboxData = microsoft_fetch_inbox_messages($userId, 30); $forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh);
$messages = $inboxData['messages'] ?? []; $messages = $inboxData['messages'] ?? [];
$isDemo = !empty($inboxData['is_demo']); $isDemo = !empty($inboxData['is_demo']);
$unreadCount = (int)($inboxData['unread_count'] ?? 0); $unreadCount = (int)($inboxData['unread_count'] ?? 0);
@@ -720,7 +721,7 @@ async function refreshMailbox() {
if (icon) icon.classList.add('animate-spin'); if (icon) icon.classList.add('animate-spin');
try { try {
const res = await fetch('/api/mail.php?action=get_inbox'); const res = await fetch('/api/mail.php?action=get_inbox&refresh=1');
const data = await res.json(); const data = await res.json();
if (data.success && data.messages) { if (data.success && data.messages) {
allMessages = data.messages; allMessages = data.messages;
+14 -4
View File
@@ -79,14 +79,24 @@ $syncIntervalSeconds = isset($outlookIntegrationSettings['sync_interval_seconds'
$weekOutlookEvents = []; $weekOutlookEvents = [];
$isLiveOutlook = false; $isLiveOutlook = false;
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
if ($isOutlookConnected) { if ($isOutlookConnected) {
try { try {
// Haal afspraken op voor het volledige datumbereik van de week // Haal afspraken op voor het volledige datumbereik van de week (met 60s cache)
$weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd); $weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd, $forceRefresh);
$isLiveOutlook = true; $isLiveOutlook = true;
// Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data // Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data
// Alleen uitvoeren indien events gewijzigd zijn of bij expliciete refresh om overtollige DB load te voorkomen
$eventsHash = md5(json_encode($weekOutlookEvents));
$lastReconciledHash = $_SESSION["last_reconciled_{$userId}"] ?? '';
if ($eventsHash !== $lastReconciledHash || $forceRefresh) {
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents); microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION["last_reconciled_{$userId}"] = $eventsHash;
}
}
} catch (Throwable $e) { } catch (Throwable $e) {
error_log("Outlook synchronisatiefout in planner: " . $e->getMessage()); error_log("Outlook synchronisatiefout in planner: " . $e->getMessage());
$isLiveOutlook = false; $isLiveOutlook = false;
@@ -224,7 +234,7 @@ for ($i = 0; $i < 7; $i++) {
$jiraIssues = []; $jiraIssues = [];
$isJiraLive = false; $isJiraLive = false;
try { try {
$jiraData = jira_get_user_issues($userId); $jiraData = jira_get_user_issues($userId, $forceRefresh);
$jiraIssues = $jiraData['issues'] ?? []; $jiraIssues = $jiraData['issues'] ?? [];
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']); $isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
} catch (Throwable $e) { } catch (Throwable $e) {
@@ -236,7 +246,7 @@ $recentEmails = [];
$unreadEmailCount = 0; $unreadEmailCount = 0;
$mailNeedsReconsent = false; $mailNeedsReconsent = false;
try { try {
$mailData = microsoft_fetch_inbox_messages($userId, 5); $mailData = microsoft_fetch_inbox_messages($userId, 5, false, $forceRefresh);
$recentEmails = $mailData['messages'] ?? []; $recentEmails = $mailData['messages'] ?? [];
$unreadEmailCount = (int)($mailData['unread_count'] ?? 0); $unreadEmailCount = (int)($mailData['unread_count'] ?? 0);
$mailNeedsReconsent = !empty($mailData['needs_reconsent']); $mailNeedsReconsent = !empty($mailData['needs_reconsent']);
+9 -7
View File
@@ -840,8 +840,8 @@ require_once __DIR__ . '/includes/header.php';
</a> </a>
</div> </div>
<div class="relative"> <div class="relative">
<input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? '••••••••••••••••' : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? htmlspecialchars($jiraSettings['jira_api_token']) : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<button type="button" onclick="toggleJiraToken()" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer"> <button type="button" onclick="toggleJiraToken()" id="toggleJiraTokenBtn" title="Toon / verberg token" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer">
<i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i> <i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i>
</button> </button>
</div> </div>
@@ -1122,16 +1122,18 @@ function updateProfileThemeUI(theme) {
function toggleJiraToken() { function toggleJiraToken() {
const input = document.getElementById('jiraApiTokenInput'); const input = document.getElementById('jiraApiTokenInput');
const icon = document.getElementById('eyeJiraIcon'); const btn = document.getElementById('toggleJiraTokenBtn');
if (!input || !icon) return; if (!input) return;
if (input.type === 'password') { if (input.type === 'password') {
input.type = 'text'; input.type = 'text';
icon.setAttribute('data-lucide', 'eye-off'); if (btn) btn.innerHTML = '<i data-lucide="eye-off" class="w-4 h-4"></i>';
} else { } else {
input.type = 'password'; input.type = 'password';
icon.setAttribute('data-lucide', 'eye'); if (btn) btn.innerHTML = '<i data-lucide="eye" class="w-4 h-4"></i>';
}
if (window.lucide && typeof lucide.createIcons === 'function') {
lucide.createIcons();
} }
if (window.lucide) lucide.createIcons();
} }
function previewUploadAvatar(input) { function previewUploadAvatar(input) {