* Copyright (C) 2026 Nick Fragoulis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY, without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/ai/admin/log_viewer.php * \ingroup ai * \brief AI Request Log Viewer with Payload Inspection */ /** * @var Conf $conf * @var DoliDB $db * @var HookManager $hookmanager * @var Translate $langs * @var User $user * @var Form $form */ require '../../main.inc.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.form.class.php'; // Access Control if (!$user->admin) { accessforbidden(); } // Load translations $langs->loadLangs(array("admin", "other")); // Parameters $action = GETPOST('action', 'aZ09'); $massaction = GETPOST('massaction', 'alpha'); $confirm = GETPOST('confirm', 'alpha'); $toselect = GETPOST('toselect', 'array'); $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'ailoglist'; $optioncss = GETPOST('optioncss', 'alpha'); $mode = GETPOST('mode', 'alpha'); // Search parameters for all columns $search_date_start = dol_mktime(0, 0, 0, GETPOSTINT('search_date_startmonth'), GETPOSTINT('search_date_startday'), GETPOSTINT('search_date_startyear')); $search_date_end = dol_mktime(23, 59, 59, GETPOSTINT('search_date_endmonth'), GETPOSTINT('search_date_endday'), GETPOSTINT('search_date_endyear')); $search_user = GETPOST('search_user', 'alpha'); $search_query = GETPOST('search_query', 'alpha'); $search_tool = GETPOST('search_tool', 'alpha'); $search_provider = GETPOST('search_provider', 'alpha'); $search_time_min = GETPOST('search_time_min', 'alpha'); $search_time_max = GETPOST('search_time_max', 'alpha'); $search_status = GETPOST('search_status', 'alpha'); // Pagination parameters $limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit; $sortfield = GETPOST('sortfield', 'alpha'); $sortorder = GETPOST('sortorder', 'alpha'); $page = GETPOSTINT("page"); if (empty($page) || $page == -1) { $page = 0; } $offset = $limit * $page; if (!$sortfield) $sortfield = "l.date_request"; if (!$sortorder) $sortorder = "DESC"; // Initialize array of search criteria $search_array = array( 'search_date_start' => $search_date_start, 'search_date_end' => $search_date_end, 'search_user' => $search_user, 'search_query' => $search_query, 'search_tool' => $search_tool, 'search_provider' => $search_provider, 'search_time_min' => $search_time_min, 'search_time_max' => $search_time_max, 'search_status' => $search_status ); /* * Actions */ $error = ''; if ($action == 'purge' && $confirm == 'yes') { $db->begin(); $sql = "DELETE FROM " . MAIN_DB_PREFIX . "ai_request_log"; $sql .= " WHERE entity IN (" . getEntity('airequestlog') . ")"; $resql = $db->query($sql); if ($resql) { $nbDeleted = $db->affected_rows($resql); $db->commit(); setEventMessages($langs->trans("LogsCleared") . " (" . $nbDeleted . ")", null, 'mesgs'); } else { $db->rollback(); setEventMessages($db->lasterror(), null, 'errors'); } header('Location: ' . $_SERVER["PHP_SELF"]); exit; } // Purge selection if ($massaction == 'purge' && !empty($toselect) && is_array($toselect)) { $db->begin(); foreach ($toselect as $id) { $sql = "DELETE FROM " . MAIN_DB_PREFIX . "ai_request_log"; $sql .= " WHERE rowid = " . ((int) $id); $sql .= " AND entity IN (" . getEntity('airequestlog') . ")"; $resql = $db->query($sql); if (!$resql) { $error++; $db->rollback(); setEventMessages($db->lasterror(), null, 'errors'); break; } } if (!$error) { $db->commit(); setEventMessages($langs->trans("SelectedLogsDeleted"), null, 'mesgs'); } else { $db->rollback(); } $action = 'list'; $massaction = ''; } // Clear filter action if (GETPOST('button_removefilter', 'alpha') || GETPOST('button_removefilter_x', 'alpha')) { $search_date_start = ''; $search_date_end = ''; $search_user = ''; $search_query = ''; $search_tool = ''; $search_provider = ''; $search_time_min = ''; $search_time_max = ''; $search_status = ''; // Reset page $page = 0; } /* * View */ // Initialize array of search criteria for the view $param = ''; if ($contextpage != $_SERVER["PHP_SELF"]) { $param .= '&contextpage='.urlencode($contextpage); } if ($limit > 0 && $limit != $conf->liste_limit) { $param .= '&limit='.urlencode($limit); } foreach ($search_array as $key => $val) { if (!empty($val) || $val === '0') { $param .= '&' . $key . '=' . urlencode($val); } } llxHeader('', $langs->trans("AIRequestLogs"), ''); // Build WHERE clause $where = array(); $where[] = "l.entity IN (" . getEntity('airequestlog') . ")"; if ($search_date_start) { $where[] = "l.date_request >= '" . $db->escape(date('Y-m-d H:i:s', $search_date_start)) . "'"; } if ($search_date_end) { $where[] = "l.date_request <= '" . $db->escape(date('Y-m-d H:i:s', $search_date_end)) . "'"; } if ($search_user) { $where[] = "u.login LIKE '%" . $db->escape($search_user) . "%'"; } if ($search_query) { $where[] = "l.query_text LIKE '%" . $db->escape($search_query) . "%'"; } if ($search_tool) { $where[] = "l.tool_name LIKE '%" . $db->escape($search_tool) . "%'"; } if ($search_provider) { $where[] = "l.provider LIKE '%" . $db->escape($search_provider) . "%'"; } if ($search_time_min) { $where[] = "l.execution_time >= " . floatval($search_time_min); } if ($search_time_max) { $where[] = "l.execution_time <= " . floatval($search_time_max); } if ($search_status) { $where[] = "l.status = '" . $db->escape($search_status) . "'"; } $whereSQL = ''; if (!empty($where)) { $whereSQL = ' WHERE ' . implode(' AND ', $where); } // Get total count for pagination $sqlCount = "SELECT COUNT(*) as total FROM " . MAIN_DB_PREFIX . "ai_request_log as l LEFT JOIN " . MAIN_DB_PREFIX . "user as u ON l.fk_user = u.rowid $whereSQL"; $resCount = $db->query($sqlCount); $totalRecords = $resCount ? $db->fetch_object($resCount)->total : 0; $sql = "SELECT l.*, u.login FROM " . MAIN_DB_PREFIX . "ai_request_log as l LEFT JOIN " . MAIN_DB_PREFIX . "user as u ON l.fk_user = u.rowid $whereSQL ORDER BY $sortfield $sortorder LIMIT " . $offset . ", " . $limit; $res = $db->query($sql); $num = $db->num_rows($res); // Create object for list $object = new stdClass(); $object->total = $totalRecords; $title = $langs->trans("AIRequestLogs"); print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $totalRecords, 'title_ai', 0, '', '', $limit, 1, 0, 0, ''); print '
'; print ''; print ''; // Add all search parameters to preserve them when changing the limit foreach ($search_array as $key => $val) { if (!empty($val) || $val === '0') { print ''; } } print '
'; print ''; print ''; print ''; print ''; print '
'; print $langs->trans("Show") . ': '; print ''; print ''; print ''; print ''; // Create array of options for limit $arrayoflimit = array(5, 10, 20, 50, 100, 500, 1000); print ''; print ' ' . $langs->trans("Entries"); print '
'; print '
'; print '
'; // Display form for filters print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'; print ''."\n"; // Fields title print ''; print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "l.date_request", "", $param, '', $sortfield, $sortorder); print_liste_field_titre("User", $_SERVER["PHP_SELF"], "u.login", "", $param, '', $sortfield, $sortorder); print_liste_field_titre("Query", $_SERVER["PHP_SELF"], "l.query_text", "", $param, '', $sortfield, $sortorder); print_liste_field_titre("MCPTool", $_SERVER["PHP_SELF"], "l.tool_name", "", $param, '', $sortfield, $sortorder); print_liste_field_titre("Provider", $_SERVER["PHP_SELF"], "l.provider", "", $param, '', $sortfield, $sortorder); print_liste_field_titre("Time", $_SERVER["PHP_SELF"], "l.execution_time", "", $param, 'align="center"', $sortfield, $sortorder); print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "l.status", "", $param, 'align="center"', $sortfield, $sortorder); print_liste_field_titre('', $_SERVER["PHP_SELF"], "", "", $param, 'align="center"'); print ''; // Search row print ''; // Date search print ''; // User search print ''; // Query search print ''; // Tool search print ''; // Provider search print ''; // Time search print ''; // Search buttons print ''; print ''; // Mass action buttons print ''; print ''; print ''; if ($res && $db->num_rows($res) > 0) { $i = 0; while ($obj = $db->fetch_object($res)) { print ''; // Date print ''; // User print ''; // Query - properly escaped $shortQuery = dol_trunc($obj->query_text, 60); print ''; // Tool print ''; // Provider print ''; // Time $timeColor = ($obj->execution_time > 5) ? 'color:red;' : ''; print ''; // Status $badge = 'badge-status0'; if ($obj->status == $langs->transnoentitiesnoconv("Success")) { $badge = 'badge-status4'; // Green } if ($obj->status == $langs->transnoentitiesnoconv("Confirm")) { $badge = 'badge-status3'; // Yellow } if ($obj->status == $langs->transnoentitiesnoconv('Error')) { $badge = 'badge-status8'; // Red } print ''; // Details Button (Triggers Modal) // We embed data attributes securely with proper UTF-8 handling $reqSafe = base64_encode($obj->raw_request_payload); $resSafe = base64_encode($obj->raw_response_payload); $errSafe = base64_encode($obj->error_msg); print ''; print ''; $i++; } } else { $colspan = 8; print ''; } print '
'; print $form->selectDate($search_date_start, 'search_date_start', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("From")); print ' - '; print $form->selectDate($search_date_end, 'search_date_end', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("To")); print ''; print ''; print ''; // Status search print ''; $status_options = array('' => $langs->trans("All"), 'success' => $langs->trans("Success"), 'confirm' => $langs->trans("Confirm"), 'error' => $langs->trans("Error")); print $form->selectarray('search_status', $status_options, $search_status, 0, 0, 0, '', 1); // @phan-suppress-current-line PhanPluginSuspiciousParamOrder print ''; $searchpicto = img_picto($langs->trans("Search"), 'search.png', '', 0, 1); print ''; $clearpicto = img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', 0, 1); print ''; print '
'; print ''; print '
' . dol_print_date($db->jdate($obj->date_request), 'dayhour') . '' . ($obj->login ? dol_escape_htmltag($obj->login) : $langs->trans("Unknown")) . '' . dol_escape_htmltag($shortQuery) . '' . dol_escape_htmltag($obj->tool_name) . '' . dol_escape_htmltag($obj->provider) . '' . round($obj->execution_time, 2) . 's' . dol_escape_htmltag($obj->status) . ''; print ''; print ' ' . $langs->trans("View"); print ''; print '
' . $langs->trans("NoLogsFound"); if (!empty($where)) { print ' ' . $langs->trans("MatchingSearchCriteria"); } print '. ' . $langs->trans("TryAskingAI") . '.
'; print '
'; // --- MODAL HTML & JS --- ?>