isLoggedIn()) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Unauthorized']); exit; } $db = Database::getInstance(); $action = $_POST['action'] ?? ''; // Create necessary tables if they don't exist createIntegrityTables($db); // Clean any output that might have been generated $output = ob_get_clean(); if (!empty($output)) { error_log("Unexpected output in integrity_check_handler: " . $output); } switch ($action) { case 'get_directives': echo json_encode(getDirectives($db)); break; case 'create_directive': echo json_encode(createDirective($db, $_POST)); break; case 'approve_directive': echo json_encode(approveDirective($db, $_POST['directive_id'])); break; case 'delete_directive': echo json_encode(deleteDirective($db, $_POST['directive_id'])); break; case 'start_integrity_check': echo json_encode(startIntegrityCheck($db)); break; case 'get_check_status': echo json_encode(getCheckStatus($db)); break; case 'get_check_progress': echo json_encode(getCheckProgress($db)); break; case 'pause_integrity_check': echo json_encode(pauseIntegrityCheck($db)); break; case 'stop_integrity_check': echo json_encode(stopIntegrityCheck($db)); break; case 'delete_affected_panelists': echo json_encode(deleteAffectedPanelists($db)); break; case 'force_clear_state': echo json_encode(forceClearState($db)); break; case 'reset_integrity_check': echo json_encode(resetIntegrityCheck($db)); break; case 'disable_stale_detection': echo json_encode(disableStaleDetection($db)); break; case 'debug_info': // Debug endpoint to help troubleshoot $debug_info = [ 'php_version' => PHP_VERSION, 'db_connected' => $db ? true : false, 'tables_exist' => [], 'post_data' => $_POST, 'current_state' => null, 'stale_detection_disabled' => checkStaleDetectionDisabled($db) ]; // Check if tables exist $tables = ['panel_directives', 'panel_integrity_checks', 'panel_integrity_state', 'panel_integrity_results']; foreach ($tables as $table) { $result = $db->query("SHOW TABLES LIKE '$table'"); $debug_info['tables_exist'][$table] = $result && $result->num_rows > 0; } // Get current state $state_result = $db->query("SELECT * FROM panel_integrity_state WHERE id = 1"); if ($state_result && $state_result->num_rows > 0) { $debug_info['current_state'] = $state_result->fetch_assoc(); } echo json_encode(['success' => true, 'debug_info' => $debug_info]); break; default: echo json_encode(['success' => false, 'message' => 'Invalid action']); break; } } catch (Exception $e) { // Clean any output buffer if (ob_get_level()) { ob_end_clean(); } error_log("Fatal error in integrity_check_handler: " . $e->getMessage()); echo json_encode(['success' => false, 'message' => 'Server error: ' . $e->getMessage()]); } function createIntegrityTables($db) { // Create panel_directives table $db->query(" CREATE TABLE IF NOT EXISTS panel_directives ( id INT AUTO_INCREMENT PRIMARY KEY, attribute1_id INT NOT NULL, attribute2_id INT NOT NULL, choice1 JSON NOT NULL, choice2 JSON NOT NULL, status ENUM('pending', 'approved') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INT, INDEX(status), INDEX(attribute1_id), INDEX(attribute2_id) ) "); // Create panel_integrity_checks table (tracks which panelist was checked against which directive) $db->query(" CREATE TABLE IF NOT EXISTS panel_integrity_checks ( id INT AUTO_INCREMENT PRIMARY KEY, panelist_id VARCHAR(50) NOT NULL, directive_id INT NOT NULL, checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_affected BOOLEAN DEFAULT FALSE, UNIQUE KEY unique_check (panelist_id, directive_id), INDEX(panelist_id), INDEX(directive_id), INDEX(is_affected) ) "); // Create panel_integrity_state table (tracks current check progress) $db->query(" CREATE TABLE IF NOT EXISTS panel_integrity_state ( id INT AUTO_INCREMENT PRIMARY KEY, is_running BOOLEAN DEFAULT FALSE, is_paused BOOLEAN DEFAULT FALSE, processed_count INT DEFAULT 0, total_count INT DEFAULT 0, current_panelist_id VARCHAR(50), current_directive_id INT, status TEXT, start_time TIMESTAMP NULL, last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) "); // Create panel_integrity_results table (stores current check results) $db->query(" CREATE TABLE IF NOT EXISTS panel_integrity_results ( id INT AUTO_INCREMENT PRIMARY KEY, check_session_id VARCHAR(50) NOT NULL, panelist_id VARCHAR(50) NOT NULL, directive_id INT NOT NULL, is_affected BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX(check_session_id), INDEX(panelist_id), INDEX(is_affected) ) "); } function getDirectives($db) { try { $query = " SELECT d.*, a1.name as attribute1_name, a2.name as attribute2_name FROM panel_directives d LEFT JOIN attributes a1 ON d.attribute1_id = a1.id LEFT JOIN attributes a2 ON d.attribute2_id = a2.id ORDER BY d.created_at DESC "; $result = $db->query($query); $directives = []; if ($result) { while ($row = $result->fetch_assoc()) { $row['choice1'] = json_decode($row['choice1'], true); $row['choice2'] = json_decode($row['choice2'], true); // Format choices for display $row['choice1'] = is_array($row['choice1']) ? implode(', ', $row['choice1']) : $row['choice1']; $row['choice2'] = is_array($row['choice2']) ? implode(', ', $row['choice2']) : $row['choice2']; $directives[] = $row; } } return ['success' => true, 'directives' => $directives]; } catch (Exception $e) { error_log("Error getting directives: " . $e->getMessage()); return ['success' => false, 'message' => 'Failed to load directives']; } } function createDirective($db, $data) { try { $attribute1_id = intval($data['attribute1_id']); $attribute2_id = intval($data['attribute2_id']); $choice1 = json_decode($data['choice1'], true); $choice2 = json_decode($data['choice2'], true); if (!$attribute1_id || !$attribute2_id || empty($choice1) || empty($choice2)) { throw new Exception("All fields are required"); } if ($attribute1_id === $attribute2_id) { throw new Exception("Please select different attributes for both conditions"); } $stmt = $db->prepare(" INSERT INTO panel_directives (attribute1_id, attribute2_id, choice1, choice2) VALUES (?, ?, ?, ?) "); $choice1_json = json_encode($choice1); $choice2_json = json_encode($choice2); $stmt->bind_param('iiss', $attribute1_id, $attribute2_id, $choice1_json, $choice2_json); if (!$stmt->execute()) { throw new Exception("Failed to create directive: " . $stmt->error); } return ['success' => true, 'message' => 'Directive created successfully']; } catch (Exception $e) { error_log("Error creating directive: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function approveDirective($db, $directiveId) { try { $stmt = $db->prepare("UPDATE panel_directives SET status = 'approved' WHERE id = ?"); $stmt->bind_param('i', $directiveId); if (!$stmt->execute()) { throw new Exception("Failed to approve directive: " . $stmt->error); } if ($stmt->affected_rows === 0) { throw new Exception("Directive not found"); } return ['success' => true, 'message' => 'Directive approved successfully']; } catch (Exception $e) { error_log("Error approving directive: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function deleteDirective($db, $directiveId) { try { // Also delete related integrity checks $db->query("DELETE FROM panel_integrity_checks WHERE directive_id = $directiveId"); $stmt = $db->prepare("DELETE FROM panel_directives WHERE id = ?"); $stmt->bind_param('i', $directiveId); if (!$stmt->execute()) { throw new Exception("Failed to delete directive: " . $stmt->error); } if ($stmt->affected_rows === 0) { throw new Exception("Directive not found"); } return ['success' => true, 'message' => 'Directive deleted successfully']; } catch (Exception $e) { error_log("Error deleting directive: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function startIntegrityCheck($db) { try { // Check if already running $result = $db->query("SELECT is_running FROM panel_integrity_state WHERE id = 1"); if ($result && $result->num_rows > 0) { $state = $result->fetch_assoc(); if ($state['is_running']) { throw new Exception("Integrity check is already running"); } } // Get approved directives $directives_result = $db->query(" SELECT id, attribute1_id, attribute2_id, choice1, choice2 FROM panel_directives WHERE status = 'approved' "); if (!$directives_result || $directives_result->num_rows === 0) { throw new Exception("No approved directives found. Please approve at least one directive first."); } // Count total panelists $count_result = $db->query("SELECT COUNT(*) as total FROM panel_data"); $total_count = $count_result ? $count_result->fetch_assoc()['total'] : 0; if ($total_count === 0) { throw new Exception("No panel members found to check."); } // Create new check session $check_session_id = uniqid('check_', true); // Reset state $db->query("DELETE FROM panel_integrity_state"); $db->query("DELETE FROM panel_integrity_results"); // Initialize state $stmt = $db->prepare(" INSERT INTO panel_integrity_state (id, is_running, is_paused, processed_count, total_count, status, start_time, last_update) VALUES (1, 1, 0, 0, ?, 'Starting integrity check...', NOW(), NOW()) "); $stmt->bind_param('i', $total_count); $stmt->execute(); // Store check session ID for later use $db->query("UPDATE panel_integrity_state SET current_panelist_id = '$check_session_id' WHERE id = 1"); error_log("Integrity check initialized: session_id=$check_session_id, total_count=$total_count"); return ['success' => true, 'message' => 'Integrity check started successfully']; } catch (Exception $e) { error_log("Error starting integrity check: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function checkPanelistAgainstDirective($attribute_values, $directive) { $attr1_id = $directive['attribute1_id']; $attr2_id = $directive['attribute2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; // Get panelist's values for these attributes $panelist_attr1 = $attribute_values[$attr1_id] ?? null; $panelist_attr2 = $attribute_values[$attr2_id] ?? null; if ($panelist_attr1 === null || $panelist_attr2 === null) { return false; // Can't check if values are missing } // Convert single values to arrays for consistent checking $panelist_attr1 = is_array($panelist_attr1) ? $panelist_attr1 : [$panelist_attr1]; $panelist_attr2 = is_array($panelist_attr2) ? $panelist_attr2 : [$panelist_attr2]; // Check if panelist has any of the forbidden combinations foreach ($panelist_attr1 as $val1) { if (in_array($val1, $choice1)) { foreach ($panelist_attr2 as $val2) { if (in_array($val2, $choice2)) { return true; // Found forbidden combination } } } } return false; } function disableStaleDetection($db) { try { // Create a simple flag table to disable stale detection for debugging $db->query(" CREATE TABLE IF NOT EXISTS debug_settings ( setting_name VARCHAR(50) PRIMARY KEY, setting_value VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) "); $db->query(" INSERT INTO debug_settings (setting_name, setting_value) VALUES ('disable_stale_detection', 'true') ON DUPLICATE KEY UPDATE setting_value = 'true', created_at = NOW() "); return ['success' => true, 'message' => 'Stale detection disabled for debugging']; } catch (Exception $e) { error_log("Error disabling stale detection: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function checkStaleDetectionDisabled($db) { try { $result = $db->query(" SELECT setting_value FROM debug_settings WHERE setting_name = 'disable_stale_detection' "); if ($result && $result->num_rows > 0) { $row = $result->fetch_assoc(); return $row['setting_value'] === 'true'; } return false; } catch (Exception $e) { return false; } } function forceClearState($db) { try { // Force clear all integrity check related tables $db->query("DELETE FROM panel_integrity_state"); $db->query("DELETE FROM panel_integrity_results"); // Also clear debug settings $db->query("DELETE FROM debug_settings WHERE setting_name = 'disable_stale_detection'"); // Also clear any processes that might be stuck $db->query("UPDATE panel_integrity_state SET is_running = 0, is_paused = 0 WHERE is_running = 1"); error_log("Force cleared all integrity check state"); return ['success' => true, 'message' => 'All integrity check state forcibly cleared']; } catch (Exception $e) { error_log("Error force clearing state: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function resetIntegrityCheck($db) { try { // Clear all integrity check state $db->query("DELETE FROM panel_integrity_state"); $db->query("DELETE FROM panel_integrity_results"); return ['success' => true, 'message' => 'Integrity check state reset successfully']; } catch (Exception $e) { error_log("Error resetting integrity check: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function getCheckStatus($db) { try { $result = $db->query("SELECT * FROM panel_integrity_state WHERE id = 1"); if (!$result || $result->num_rows === 0) { return [ 'success' => true, 'is_running' => false, 'is_paused' => false, 'progress' => 0, 'status' => 'Ready to start integrity check' ]; } $state = $result->fetch_assoc(); // Add extensive debugging $start_time = strtotime($state['start_time']); $current_time = time(); $last_update = strtotime($state['last_update']); $time_since_start = $current_time - $start_time; $time_since_update = $current_time - $last_update; // Check if stale detection is disabled $stale_detection_disabled = checkStaleDetectionDisabled($db); // Log everything for debugging error_log("=== INTEGRITY CHECK STATUS DEBUG ==="); error_log("Raw start_time from DB: " . $state['start_time']); error_log("Raw last_update from DB: " . $state['last_update']); error_log("Parsed start_time: $start_time"); error_log("Parsed last_update: $last_update"); error_log("Current time: $current_time"); error_log("Time since start: $time_since_start seconds"); error_log("Time since update: $time_since_update seconds"); error_log("Is running: " . ($state['is_running'] ? 'true' : 'false')); error_log("Processed: {$state['processed_count']}/{$state['total_count']}"); error_log("Stale detection disabled: " . ($stale_detection_disabled ? 'true' : 'false')); // FOR NOW: COMPLETELY DISABLE STALE DETECTION TO TEST // This will help us isolate if the issue is in the stale detection logic $is_stale = false; if (false) { // Temporarily disable ALL stale detection // DON'T check for stale state if: // 1. The check is very new (less than 30 seconds old), OR // 2. Stale detection is disabled for debugging if ($state['is_running'] && $time_since_start > 30 && !$stale_detection_disabled) { // Much more conservative stale detection - only for checks that have had time to start: if ($time_since_start > 1200) { // 20 minutes total $is_stale = true; error_log("STALE REASON: running too long ($time_since_start seconds)"); } elseif ($time_since_update > 600 && $time_since_start > 300) { // 10 minutes no update, after 5 minutes $is_stale = true; error_log("STALE REASON: no recent updates (running $time_since_start seconds, last update $time_since_update seconds ago)"); } elseif ($state['processed_count'] == 0 && $time_since_start > 600) { // No progress in 10 minutes $is_stale = true; error_log("STALE REASON: no progress after 10 minutes (running $time_since_start seconds)"); } } } error_log("Is stale: " . ($is_stale ? 'true' : 'false')); error_log("=== END DEBUG ==="); if ($is_stale) { // Force clear the stale state $db->query("DELETE FROM panel_integrity_state"); error_log("Cleared stale integrity check state"); return [ 'success' => true, 'is_running' => false, 'is_paused' => false, 'progress' => 0, 'status' => 'Previous check timed out and was cleared', 'was_stale' => true ]; } $progress = $state['total_count'] > 0 ? ($state['processed_count'] / $state['total_count']) * 100 : 0; return [ 'success' => true, 'is_running' => (bool)$state['is_running'], 'is_paused' => (bool)$state['is_paused'], 'progress' => round($progress, 2), 'status' => $state['status'], 'processed_count' => $state['processed_count'], 'total_count' => $state['total_count'], 'time_since_start' => $time_since_start, 'debug_info' => [ 'start_time' => $state['start_time'], 'current_time' => date('Y-m-d H:i:s', $current_time), 'time_since_start' => $time_since_start, 'stale_detection_disabled' => $stale_detection_disabled ] ]; } catch (Exception $e) { error_log("Error getting check status: " . $e->getMessage()); return ['success' => false, 'message' => 'Failed to get check status']; } } function getCheckProgress($db) { try { $status_result = getCheckStatus($db); if (!$status_result['success']) { return $status_result; } // If running, process some panelists if ($status_result['is_running'] && !$status_result['is_paused']) { processIntegrityCheckChunk($db); // Get updated status $status_result = getCheckStatus($db); } $progress = $status_result['progress']; // If completed, get results if (!$status_result['is_running'] && $progress >= 100) { $affected_result = $db->query(" SELECT COUNT(DISTINCT panelist_id) as affected_count FROM panel_integrity_results WHERE is_affected = 1 "); $directives_result = $db->query(" SELECT COUNT(*) as directives_count FROM panel_directives WHERE status = 'approved' "); $affected_count = $affected_result ? $affected_result->fetch_assoc()['affected_count'] : 0; $directives_count = $directives_result ? $directives_result->fetch_assoc()['directives_count'] : 0; $status_result['results'] = [ 'affected_count' => $affected_count, 'directives_checked' => $directives_count, 'total_checked' => $status_result['processed_count'] ]; } return $status_result; } catch (Exception $e) { error_log("Error getting check progress: " . $e->getMessage()); return ['success' => false, 'message' => 'Failed to get check progress']; } } function processIntegrityCheckChunk($db) { try { // Get current state $state_result = $db->query("SELECT * FROM panel_integrity_state WHERE id = 1"); if (!$state_result || $state_result->num_rows === 0) { error_log("No integrity check state found"); return; } $state = $state_result->fetch_assoc(); if (!$state['is_running'] || $state['is_paused']) { error_log("Integrity check not running or paused"); return; } $check_session_id = $state['current_panelist_id']; // We stored session ID here $processed_count = $state['processed_count']; $total_count = $state['total_count']; error_log("Processing chunk: $processed_count/$total_count"); // Process up to 5 panelists at a time (smaller chunks for better responsiveness) $chunk_size = 5; // Get approved directives $directives_result = $db->query(" SELECT id, attribute1_id, attribute2_id, choice1, choice2 FROM panel_directives WHERE status = 'approved' "); $directives = []; while ($row = $directives_result->fetch_assoc()) { $row['choice1'] = json_decode($row['choice1'], true); $row['choice2'] = json_decode($row['choice2'], true); $directives[] = $row; } if (empty($directives)) { // No directives to check error_log("No approved directives found"); $db->query("UPDATE panel_integrity_state SET is_running = 0, status = 'No approved directives found' WHERE id = 1"); return; } error_log("Found " . count($directives) . " approved directives"); // Get panelists to process (skip already processed ones) $panelists_result = $db->query(" SELECT panelist_id, attribute_values FROM panel_data LIMIT $processed_count, $chunk_size "); if (!$panelists_result || $panelists_result->num_rows === 0) { // No more panelists to process - mark as completed $affected_result = $db->query(" SELECT COUNT(DISTINCT panelist_id) as affected_count FROM panel_integrity_results WHERE is_affected = 1 "); $affected_count = $affected_result ? $affected_result->fetch_assoc()['affected_count'] : 0; error_log("Integrity check completed with $affected_count affected panelists"); $db->query(" UPDATE panel_integrity_state SET is_running = 0, status = 'Integrity check completed. Found $affected_count affected panelists.' WHERE id = 1 "); return; } $chunk_processed = 0; $chunk_affected = 0; while ($panelist = $panelists_result->fetch_assoc()) { $panelist_id = $panelist['panelist_id']; $attribute_values = json_decode($panelist['attribute_values'], true); $panelist_affected = false; error_log("Checking panelist: $panelist_id"); foreach ($directives as $directive) { // Check if this panelist was already checked against this directive $check_result = $db->query(" SELECT id FROM panel_integrity_checks WHERE panelist_id = '$panelist_id' AND directive_id = {$directive['id']} "); if ($check_result && $check_result->num_rows > 0) { continue; // Skip - already checked } // Check if panelist matches this directive (has forbidden combination) $matches_directive = checkPanelistAgainstDirective($attribute_values, $directive); // Record the check $affected_flag = $matches_directive ? 1 : 0; $db->query(" INSERT INTO panel_integrity_checks (panelist_id, directive_id, is_affected) VALUES ('$panelist_id', {$directive['id']}, $affected_flag) ON DUPLICATE KEY UPDATE is_affected = $affected_flag "); if ($matches_directive) { $panelist_affected = true; // Record in current session results $db->query(" INSERT INTO panel_integrity_results (check_session_id, panelist_id, directive_id, is_affected) VALUES ('$check_session_id', '$panelist_id', {$directive['id']}, 1) ON DUPLICATE KEY UPDATE is_affected = 1 "); error_log("Panelist $panelist_id is affected by directive {$directive['id']}"); } } if ($panelist_affected) { $chunk_affected++; } $chunk_processed++; } // Update progress $new_processed_count = $processed_count + $chunk_processed; $progress = ($new_processed_count / $total_count) * 100; // Get total affected count so far $total_affected_result = $db->query(" SELECT COUNT(DISTINCT panelist_id) as affected_count FROM panel_integrity_results WHERE is_affected = 1 "); $total_affected = $total_affected_result ? $total_affected_result->fetch_assoc()['affected_count'] : 0; error_log("Progress update: $new_processed_count/$total_count processed, $total_affected affected"); $db->query(" UPDATE panel_integrity_state SET processed_count = $new_processed_count, status = 'Checked $new_processed_count of $total_count panelists. Found $total_affected affected.', last_update = NOW() WHERE id = 1 "); } catch (Exception $e) { error_log("Error processing integrity check chunk: " . $e->getMessage()); $db->query(" UPDATE panel_integrity_state SET is_running = 0, status = 'Error: " . $db->escape($e->getMessage()) . "' WHERE id = 1 "); } } function pauseIntegrityCheck($db) { try { $result = $db->query("SELECT is_running, is_paused FROM panel_integrity_state WHERE id = 1"); if (!$result || $result->num_rows === 0) { throw new Exception("No integrity check is currently running"); } $state = $result->fetch_assoc(); if (!$state['is_running']) { throw new Exception("No integrity check is currently running"); } $new_paused_state = !$state['is_paused']; $status_text = $new_paused_state ? 'Integrity check paused' : 'Integrity check resumed'; $db->query(" UPDATE panel_integrity_state SET is_paused = " . ($new_paused_state ? 1 : 0) . ", status = '$status_text' WHERE id = 1 "); return ['success' => true, 'message' => $status_text]; } catch (Exception $e) { error_log("Error pausing integrity check: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function stopIntegrityCheck($db) { try { $db->query(" UPDATE panel_integrity_state SET is_running = 0, is_paused = 0, status = 'Integrity check stopped by user' WHERE id = 1 "); return ['success' => true, 'message' => 'Integrity check stopped successfully']; } catch (Exception $e) { error_log("Error stopping integrity check: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } function deleteAffectedPanelists($db) { try { // Get affected panelist IDs $affected_result = $db->query(" SELECT DISTINCT panelist_id FROM panel_integrity_results WHERE is_affected = 1 "); if (!$affected_result || $affected_result->num_rows === 0) { throw new Exception("No affected panelists found"); } $affected_ids = []; while ($row = $affected_result->fetch_assoc()) { $affected_ids[] = "'" . $row['panelist_id'] . "'"; } $ids_list = implode(',', $affected_ids); $deleted_count = count($affected_ids); // Delete from panel_data $db->query("DELETE FROM panel_data WHERE panelist_id IN ($ids_list)"); // Clean up integrity check records for deleted panelists $db->query("DELETE FROM panel_integrity_checks WHERE panelist_id IN ($ids_list)"); $db->query("DELETE FROM panel_integrity_results WHERE panelist_id IN ($ids_list)"); // Reset integrity check state $db->query("DELETE FROM panel_integrity_state"); return [ 'success' => true, 'message' => "Successfully deleted $deleted_count affected panelists", 'deleted_count' => $deleted_count ]; } catch (Exception $e) { error_log("Error deleting affected panelists: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } ?>