feat: AI usage logging (AiLogger, ai_logs table, admin view)

- AiLogger.php: new service, inserts into ai_logs on every AI call
- ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms)
- AiBot: logs chat, media_response, nlu calls with timing and token counts
- MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision
- /admin/ai-logs: paginated table with provider/company filter
- Layout.php: added IA Logs nav link
- index.php: require AiLogger before AiBot; added GET /admin/ai-logs route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-05 09:03:21 -05:00
co-authored by Claude Sonnet 4.6
parent 1d9484a1e0
commit 5506ebd2af
7 changed files with 246 additions and 10 deletions
+110
View File
@@ -5691,4 +5691,114 @@ HTML;
}
exit;
}
public static function aiLogs(): void
{
SessionAuth::require();
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$provider = $_GET['provider'] ?? '';
$company = (int)($_GET['company'] ?? 0);
$where = [];
$params = [];
if ($provider !== '') { $where[] = 'provider = ?'; $params[] = $provider; }
if ($company > 0) { $where[] = 'company_id = ?'; $params[] = $company; }
$wSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int)db()->prepare("SELECT COUNT(*) FROM ai_logs $wSql")->execute($params) ? db()->prepare("SELECT COUNT(*) FROM ai_logs $wSql")->execute($params) && 0 : 0;
$stmt = db()->prepare("SELECT COUNT(*) FROM ai_logs $wSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$stmt2 = db()->prepare("SELECT * FROM ai_logs $wSql ORDER BY id DESC LIMIT $limit OFFSET $offset");
$stmt2->execute($params);
$rows = $stmt2->fetchAll(\PDO::FETCH_ASSOC);
$companies = db()->query("SELECT id, name FROM companies ORDER BY name")->fetchAll(\PDO::FETCH_ASSOC);
$companyMap = [];
foreach ($companies as $c) $companyMap[(int)$c['id']] = self::h($c['name']);
$pages = (int)ceil($total / $limit);
$companyOptions = '<option value="">Todas las empresas</option>';
foreach ($companies as $c) {
$sel = ($company === (int)$c['id']) ? ' selected' : '';
$companyOptions .= '<option value="' . (int)$c['id'] . '"' . $sel . '>' . self::h($c['name']) . '</option>';
}
$providerOptions = '';
foreach (['', 'openai', 'gemini', 'claude'] as $p) {
$label = $p === '' ? 'Todos' : ucfirst($p);
$sel = $provider === $p ? ' selected' : '';
$providerOptions .= "<option value=\"{$p}\"{$sel}>{$label}</option>";
}
$rows_html = '';
foreach ($rows as $r) {
$cid = (int)($r['company_id'] ?? 0);
$cname = $companyMap[$cid] ?? '<span style="color:#aaa">—</span>';
$badge = match($r['provider'] ?? '') {
'openai' => 'background:#10a37f;color:#fff',
'gemini' => 'background:#4285f4;color:#fff',
'claude' => 'background:#d97706;color:#fff',
default => 'background:#6b7280;color:#fff',
};
$tok = ($r['tokens_in'] ?? null) !== null
? self::h($r['tokens_in']) . '+' . self::h($r['tokens_out'])
: '—';
$rows_html .= '<tr>'
. '<td style="color:#9ca3af;font-size:11px">' . self::h(substr($r['created_at'] ?? '', 0, 16)) . '</td>'
. '<td>' . $cname . '</td>'
. '<td>' . self::h($r['phone_number'] ?? '') . '</td>'
. '<td><span style="' . $badge . ';border-radius:4px;padding:2px 7px;font-size:11px">' . self::h($r['provider']) . '</span></td>'
. '<td style="font-size:11px">' . self::h($r['model'] ?? '') . '</td>'
. '<td><span style="background:#f3f4f6;border-radius:4px;padding:2px 6px;font-size:11px">' . self::h($r['call_type'] ?? '') . '</span></td>'
. '<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' . self::h($r['input_preview'] ?? '') . '">' . self::h(mb_substr($r['input_preview'] ?? '', 0, 60)) . '</td>'
. '<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' . self::h($r['output_preview'] ?? '') . '">' . self::h(mb_substr($r['output_preview'] ?? '', 0, 60)) . '</td>'
. '<td style="text-align:right;font-size:11px">' . $tok . '</td>'
. '<td style="text-align:right;font-size:11px">' . number_format((int)($r['duration_ms'] ?? 0)) . 'ms</td>'
. '</tr>';
}
$pager = '';
for ($i = 1; $i <= $pages; $i++) {
$active = $i === $page ? ' style="font-weight:bold"' : '';
$url = '/admin/ai-logs?page=' . $i . ($provider ? '&provider=' . urlencode($provider) : '') . ($company ? '&company=' . $company : '');
$pager .= "<a href=\"{$url}\"{$active} class=\"btn-sm\">{$i}</a> ";
}
echo Layout::page('IA Logs', 'ai-logs', '
<style>
.filter-bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:16px}
.filter-bar select{padding:5px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px}
.log-table{width:100%;border-collapse:collapse;font-size:13px}
.log-table th{background:#f9fafb;padding:8px 10px;text-align:left;border-bottom:2px solid #e5e7eb;font-size:11px;text-transform:uppercase;color:#6b7280;white-space:nowrap}
.log-table td{padding:7px 10px;border-bottom:1px solid #f3f4f6;vertical-align:middle}
.log-table tr:hover td{background:#fafafa}
@media(prefers-color-scheme:dark){.log-table th{background:#1f2937;border-color:#374151;color:#9ca3af}.log-table td{border-color:#1f2937}.log-table tr:hover td{background:#111827}}
</style>
<div class="card" style="padding:20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="margin:0">🤖 IA Logs <span style="font-size:14px;color:#9ca3af;font-weight:400">(' . number_format($total) . ' registros)</span></h2>
</div>
<form class="filter-bar" method="get">
<select name="provider" onchange="this.form.submit()">' . $providerOptions . '</select>
<select name="company" onchange="this.form.submit()">' . $companyOptions . '</select>
</form>
<div style="overflow-x:auto">
<table class="log-table">
<thead><tr>
<th>Fecha</th><th>Empresa</th><th>Teléfono</th><th>Proveedor</th><th>Modelo</th>
<th>Tipo</th><th>Entrada</th><th>Salida</th><th>Tokens</th><th>Duración</th>
</tr></thead>
<tbody>' . ($rows_html ?: '<tr><td colspan="10" style="text-align:center;color:#9ca3af;padding:30px">Sin registros</td></tr>') . '</tbody>
</table>
</div>
' . ($pages > 1 ? '<div style="margin-top:12px">' . $pager . '</div>' : '') . '
</div>
');
}
}