Enhance Radar functionality: add new columns for AI analysis and Gmail message ID in the radar_threads table, update Radar controller to handle separate analysis requests, and improve UI to reflect pending AI analyses. Adjust sync service to store Gmail message IDs for future analysis and refine comments for clarity.
This commit is contained in:
@ -1,10 +1,14 @@
|
||||
-- MSOP-3 — colonnes analyse IA dossards (dev)
|
||||
-- Exécuter sur la BD CRM de DEV après IDEE-4-radar-proto.sql
|
||||
-- Si « Duplicate column » sur analysis : OK déjà fait — lancer seulement le ADD gmail_message_id.
|
||||
|
||||
ALTER TABLE radar_threads
|
||||
ADD COLUMN analysis TEXT NULL AFTER summary,
|
||||
ADD COLUMN analysis_delta TEXT NULL AFTER analysis;
|
||||
|
||||
-- Agrandir le résumé court (snippet) si besoin
|
||||
ALTER TABLE radar_threads
|
||||
MODIFY summary VARCHAR(1000) NOT NULL DEFAULT '';
|
||||
|
||||
-- Id message Gmail pour relire le corps lors de l’analyse IA (hors Sync).
|
||||
ALTER TABLE radar_threads
|
||||
ADD COLUMN gmail_message_id VARCHAR(64) NOT NULL DEFAULT '' AFTER gmail_thread_id;
|
||||
|
||||
5
sql/MSOP-3-radar-gmail-message-id.sql
Normal file
5
sql/MSOP-3-radar-gmail-message-id.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- MSOP-3 suite — uniquement si analysis existe déjà (erreur 1060 avant).
|
||||
-- À lancer une fois sur DEV.
|
||||
|
||||
ALTER TABLE radar_threads
|
||||
ADD COLUMN gmail_message_id VARCHAR(64) NOT NULL DEFAULT '' AFTER gmail_thread_id;
|
||||
@ -33,6 +33,6 @@ class Radar extends BaseConfig
|
||||
|
||||
public string $gmailScope = 'https://www.googleapis.com/auth/gmail.readonly';
|
||||
|
||||
/** Max analyses IA dossards par run de sync (garde-fou coût). */
|
||||
public int $maxAiAnalysesPerSync = 15;
|
||||
/** Max analyses IA dossards par clic « Analyser » (garde-fou temps HTTP). */
|
||||
public int $maxAiAnalysesPerSync = 3;
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ $routes->get('/', 'Home::index');
|
||||
// IDEE-5 — proto Radar (écran CRM v4)
|
||||
$routes->get('radar', 'Radar::index');
|
||||
$routes->get('radar/feed', 'Radar::feed');
|
||||
$routes->get('radar/sync', 'Radar::sync'); // MSOP-4 — déclenche sync allowlist (auth requise)
|
||||
$routes->get('radar/sync', 'Radar::sync'); // MSOP-4 — Gmail allowlist (rapide)
|
||||
$routes->get('radar/analyze', 'Radar::analyze'); // MSOP-3 — IA dossards (séparé)
|
||||
|
||||
$routes->get('inventaire/redirect/(:segment)', 'Inventaire::redirect/$1');
|
||||
$routes->get('inventaire/test/(:segment)', 'Inventaire::test/$1');
|
||||
|
||||
@ -8,7 +8,7 @@ use Config\Radar as RadarConfig;
|
||||
|
||||
/**
|
||||
* IDEE-5 / MSOP-2 — proto écran Radar (CI4 / Raven).
|
||||
* MSOP-4 — sync Gmail allowlist.
|
||||
* MSOP-4 — sync Gmail. MSOP-3 — analyse IA (route séparée).
|
||||
*/
|
||||
class Radar extends BaseController
|
||||
{
|
||||
@ -18,18 +18,24 @@ class Radar extends BaseController
|
||||
$ready = $model->tablesExist();
|
||||
$cfg = config(RadarConfig::class);
|
||||
|
||||
// Flash session souvent perdu sous /v4/ — fallback query string.
|
||||
$syncMsg = session()->getFlashdata('radar_sync_msg');
|
||||
if ($syncMsg === null || $syncMsg === '') {
|
||||
$syncMsg = $this->request->getGet('sync_msg');
|
||||
}
|
||||
|
||||
$pendingAi = 0;
|
||||
if ($ready && $model->hasAnalysisColumns()) {
|
||||
$pendingAi = $model->countThreadsNeedingDossardAnalysis();
|
||||
}
|
||||
|
||||
return view('radar/index', [
|
||||
'ready' => $ready,
|
||||
'exceptions' => $ready ? $model->listExceptions() : [],
|
||||
'threads' => $ready ? $model->listThreads() : [],
|
||||
'feedUrl' => site_url('v4/radar/feed'),
|
||||
'syncUrl' => site_url('v4/radar/sync'),
|
||||
'analyzeUrl' => site_url('v4/radar/analyze'),
|
||||
'pendingAi' => $pendingAi,
|
||||
'allowlist' => $cfg->mailboxAllowlist,
|
||||
'syncMsg' => is_string($syncMsg) ? $syncMsg : null,
|
||||
]);
|
||||
@ -53,7 +59,7 @@ class Radar extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Gmail → radar_* (mailbox allowlist uniquement).
|
||||
* Sync Gmail → radar_* (rapide, sans LLM).
|
||||
*/
|
||||
public function sync()
|
||||
{
|
||||
@ -80,28 +86,47 @@ class Radar extends BaseController
|
||||
$msg .= ' | ' . ($row['mailbox'] ?? '?') . ': ' . ($row['error'] ?? 'fail');
|
||||
}
|
||||
}
|
||||
if (! empty($result['ai'])) {
|
||||
$ai = $result['ai'];
|
||||
if (! empty($ai['enabled'])) {
|
||||
$msg .= sprintf(
|
||||
' | IA dossards: %d analysé(s)',
|
||||
(int) ($ai['ran'] ?? 0)
|
||||
);
|
||||
if (! empty($ai['model'])) {
|
||||
$msg .= ' [' . $ai['model'] . ']';
|
||||
}
|
||||
if (! empty($ai['errors'])) {
|
||||
$msg .= ', ' . (int) $ai['errors'] . ' erreur(s)';
|
||||
}
|
||||
if (! empty($ai['note']) && (int) ($ai['errors'] ?? 0) > 0) {
|
||||
$msg .= ' (' . \mb_substr((string) $ai['note'], 0, 120) . ')';
|
||||
}
|
||||
} else {
|
||||
$msg .= ' | IA: ' . ($ai['note'] ?: 'off');
|
||||
$msg .= ' — pour l’IA : bouton Analyser dossards';
|
||||
|
||||
return $this->redirectWithMsg($msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* MSOP-3 — analyse IA des fils dossards en attente (peut prendre 1–2 min).
|
||||
*/
|
||||
public function analyze()
|
||||
{
|
||||
$result = (new RadarSyncService())->analyzePending();
|
||||
|
||||
if ($this->request->isAJAX() || $this->request->getGet('json') !== null) {
|
||||
return $this->response->setJSON($result);
|
||||
}
|
||||
|
||||
if (! empty($result['error'])) {
|
||||
$msg = 'Analyse IA en erreur — ' . $result['error'];
|
||||
} else {
|
||||
$msg = sprintf(
|
||||
'Analyse IA : %d fait(s), %d encore en attente [%s]',
|
||||
(int) ($result['ran'] ?? 0),
|
||||
(int) ($result['pending'] ?? 0),
|
||||
(string) ($result['model'] ?? '')
|
||||
);
|
||||
if (! empty($result['errors'])) {
|
||||
$msg .= ', ' . (int) $result['errors'] . ' erreur(s)';
|
||||
}
|
||||
if (! empty($result['note']) && (int) ($result['errors'] ?? 0) > 0) {
|
||||
$msg .= ' (' . \mb_substr((string) $result['note'], 0, 120) . ')';
|
||||
}
|
||||
if ((int) ($result['pending'] ?? 0) > 0) {
|
||||
$msg .= ' — recliquer Analyser pour continuer';
|
||||
}
|
||||
}
|
||||
|
||||
// Message aussi en query : visible même si la flash session CI4 ne tient pas.
|
||||
return $this->redirectWithMsg($msg);
|
||||
}
|
||||
|
||||
private function redirectWithMsg(string $msg)
|
||||
{
|
||||
$q = rawurlencode(mb_substr($msg, 0, 800));
|
||||
|
||||
return redirect()->to(site_url('v4/radar') . '?sync_msg=' . $q)->with('radar_sync_msg', $msg);
|
||||
|
||||
@ -8,7 +8,7 @@ use Config\Radar as RadarConfig;
|
||||
|
||||
/**
|
||||
* MSOP-4 — sync Gmail allowlist → radar_threads.
|
||||
* MSOP-3 — analyse IA dossards via couche Ai plateforme.
|
||||
* MSOP-3 — analyse IA dossards via couche Ai (appel séparé, pas dans le Sync HTTP).
|
||||
*/
|
||||
class RadarSyncService
|
||||
{
|
||||
@ -21,7 +21,9 @@ class RadarSyncService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool, mailboxes:list<array<string,mixed>>, error?:string, ai?:array<string,mixed>}
|
||||
* Lecture Gmail uniquement (rapide). Pas d’appel LLM ici.
|
||||
*
|
||||
* @return array{ok:bool, mailboxes:list<array<string,mixed>>, error?:string}
|
||||
*/
|
||||
public function sync(): array
|
||||
{
|
||||
@ -43,27 +45,7 @@ class RadarSyncService
|
||||
return ['ok' => false, 'mailboxes' => [], 'error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
$analyzer = null;
|
||||
$aiStats = ['enabled' => false, 'ran' => 0, 'skipped' => 0, 'errors' => 0, 'note' => '', 'model' => ''];
|
||||
try {
|
||||
if (! $this->model->hasAnalysisColumns()) {
|
||||
$aiStats['note'] = 'Colonnes analysis absentes — exécuter sql/MSOP-3-radar-analysis.sql';
|
||||
} else {
|
||||
$ai = AiFactory::client();
|
||||
if ($ai->isConfigured()) {
|
||||
$analyzer = new RadarAnalyzeService($ai);
|
||||
$aiStats['enabled'] = true;
|
||||
$aiStats['model'] = $ai->model();
|
||||
} else {
|
||||
$aiStats['note'] = 'IA non configurée (clé absente)';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$aiStats['note'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$aiBudget = $this->config->maxAiAnalysesPerSync;
|
||||
$results = [];
|
||||
|
||||
foreach ($this->config->mailboxAllowlist as $mailbox) {
|
||||
$mailbox = \strtolower(\trim($mailbox));
|
||||
@ -80,7 +62,6 @@ class RadarSyncService
|
||||
$updated = 0;
|
||||
foreach ($threads as $t) {
|
||||
$skills = $this->guessSkills($t['subject'] . ' ' . $t['snippet']);
|
||||
$status = 'a_classer';
|
||||
$n = $this->model->upsertThread([
|
||||
'gmail_thread_id' => $t['gmail_thread_id'],
|
||||
'mailbox' => $mailbox,
|
||||
@ -89,52 +70,22 @@ class RadarSyncService
|
||||
'from_name' => \mb_substr($t['from_name'], 0, 190),
|
||||
'gmail_url' => \mb_substr($t['gmail_url'], 0, 500),
|
||||
'summary' => \mb_substr($t['snippet'], 0, 1000),
|
||||
'status' => $status,
|
||||
'status' => 'a_classer',
|
||||
'confidence' => $skills === [] ? 20 : 45,
|
||||
'is_example' => 0,
|
||||
'received_at' => $t['received_at'],
|
||||
'proposed_client_no' => '',
|
||||
'gmail_message_id' => $t['gmail_message_id'] ?? '',
|
||||
], $skills);
|
||||
// Stocker l’id message pour l’analyse IA ultérieure
|
||||
if (! empty($t['gmail_message_id'])) {
|
||||
$this->model->saveGmailMessageId($n['id'], (string) $t['gmail_message_id']);
|
||||
}
|
||||
if ($n['op'] === 'insert') {
|
||||
$inserted++;
|
||||
} else {
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (
|
||||
$analyzer
|
||||
&& \in_array('dossards', $skills, true)
|
||||
&& $aiBudget > 0
|
||||
&& ! empty($t['gmail_message_id'])
|
||||
&& $this->model->needsAnalysis($n['id'])
|
||||
) {
|
||||
try {
|
||||
$body = $reader->getPlainBody($mailbox, $t['gmail_message_id']);
|
||||
$priors = $this->model->listPriorDossardThreads(
|
||||
$t['from_email'],
|
||||
$t['gmail_thread_id']
|
||||
);
|
||||
$aiResult = $analyzer->analyzeDossards(
|
||||
$t['subject'],
|
||||
$t['from_email'],
|
||||
$body !== '' ? $body : $t['snippet'],
|
||||
$priors
|
||||
);
|
||||
$this->model->saveAnalysis(
|
||||
$n['id'],
|
||||
$aiResult['analysis'],
|
||||
$aiResult['delta'],
|
||||
$aiResult['confidence']
|
||||
);
|
||||
$aiStats['ran']++;
|
||||
$aiBudget--;
|
||||
} catch (\Throwable $e) {
|
||||
$aiStats['errors']++;
|
||||
$aiStats['note'] = $e->getMessage();
|
||||
}
|
||||
} elseif (\in_array('dossards', $skills, true) && $analyzer && $aiBudget <= 0) {
|
||||
$aiStats['skipped']++;
|
||||
}
|
||||
}
|
||||
$results[] = [
|
||||
'mailbox' => $mailbox,
|
||||
@ -163,7 +114,125 @@ class RadarSyncService
|
||||
return [
|
||||
'ok' => $anyOk || $results === [],
|
||||
'mailboxes' => $results,
|
||||
'ai' => $aiStats,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* MSOP-3 — analyse les fils dossards sans analysis (batch limité).
|
||||
*
|
||||
* @return array{ok:bool, ran:int, pending:int, errors:int, model:string, note:string, error?:string}
|
||||
*/
|
||||
public function analyzePending(): array
|
||||
{
|
||||
if (! $this->model->tablesExist()) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'ran' => 0,
|
||||
'pending' => 0,
|
||||
'errors' => 0,
|
||||
'model' => '',
|
||||
'note' => '',
|
||||
'error' => 'Tables radar_* absentes',
|
||||
];
|
||||
}
|
||||
|
||||
if (! $this->model->hasAnalysisColumns()) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'ran' => 0,
|
||||
'pending' => 0,
|
||||
'errors' => 0,
|
||||
'model' => '',
|
||||
'note' => '',
|
||||
'error' => 'Colonnes analysis absentes — sql/MSOP-3-radar-analysis.sql',
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$ai = AiFactory::client();
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'ran' => 0,
|
||||
'pending' => 0,
|
||||
'errors' => 0,
|
||||
'model' => '',
|
||||
'note' => '',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
if (! $ai->isConfigured()) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'ran' => 0,
|
||||
'pending' => 0,
|
||||
'errors' => 0,
|
||||
'model' => '',
|
||||
'note' => '',
|
||||
'error' => 'IA non configurée (clé absente)',
|
||||
];
|
||||
}
|
||||
|
||||
@\set_time_limit(300);
|
||||
|
||||
$jsonPath = \realpath(ROOTPATH . $this->config->serviceAccountJson)
|
||||
?: (ROOTPATH . $this->config->serviceAccountJson);
|
||||
$tokenClient = new GoogleWorkspaceToken($jsonPath);
|
||||
$reader = new GmailReader($tokenClient, $this->config->gmailScope);
|
||||
$analyzer = new RadarAnalyzeService($ai);
|
||||
|
||||
$budget = $this->config->maxAiAnalysesPerSync;
|
||||
$pending = $this->model->listThreadsNeedingDossardAnalysis($budget + 20);
|
||||
$ran = 0;
|
||||
$errors = 0;
|
||||
$note = '';
|
||||
|
||||
foreach ($pending as $row) {
|
||||
if ($ran + $errors >= $budget) {
|
||||
break;
|
||||
}
|
||||
$msgId = (string) ($row['gmail_message_id'] ?? '');
|
||||
if ($msgId === '') {
|
||||
// Pas d’id message stocké : on ne peut pas relire le corps ; skip propre.
|
||||
$errors++;
|
||||
$note = 'gmail_message_id manquant — re-Sync Gmail puis Analyser';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$body = $reader->getPlainBody((string) $row['mailbox'], $msgId);
|
||||
$priors = $this->model->listPriorDossardThreads(
|
||||
(string) $row['from_email'],
|
||||
(string) $row['gmail_thread_id']
|
||||
);
|
||||
$aiResult = $analyzer->analyzeDossards(
|
||||
(string) $row['subject'],
|
||||
(string) $row['from_email'],
|
||||
$body !== '' ? $body : (string) $row['summary'],
|
||||
$priors
|
||||
);
|
||||
$this->model->saveAnalysis(
|
||||
(int) $row['id'],
|
||||
$aiResult['analysis'],
|
||||
$aiResult['delta'],
|
||||
$aiResult['confidence']
|
||||
);
|
||||
$ran++;
|
||||
} catch (\Throwable $e) {
|
||||
$errors++;
|
||||
$note = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$still = $this->model->countThreadsNeedingDossardAnalysis();
|
||||
|
||||
return [
|
||||
'ok' => $errors === 0 || $ran > 0,
|
||||
'ran' => $ran,
|
||||
'pending' => $still,
|
||||
'errors' => $errors,
|
||||
'model' => $ai->model(),
|
||||
'note' => $note,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,67 @@ class RadarModel
|
||||
return $cached;
|
||||
}
|
||||
|
||||
public function hasGmailMessageIdColumn(): bool
|
||||
{
|
||||
static $cached = null;
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
$row = $this->db()->query("SHOW COLUMNS FROM radar_threads LIKE 'gmail_message_id'")->getRowArray();
|
||||
$cached = ! empty($row);
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
public function saveGmailMessageId(int $threadId, string $messageId): void
|
||||
{
|
||||
if (! $this->hasGmailMessageIdColumn() || $messageId === '') {
|
||||
return;
|
||||
}
|
||||
$this->db()->table('radar_threads')->where('id', $threadId)->update([
|
||||
'gmail_message_id' => \mb_substr($messageId, 0, 64),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fils dossards sans analyse IA, du plus récent au plus ancien.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listThreadsNeedingDossardAnalysis(int $limit = 20): array
|
||||
{
|
||||
if (! $this->hasAnalysisColumns()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$msgCol = $this->hasGmailMessageIdColumn() ? ', t.gmail_message_id' : ', \'\' AS gmail_message_id';
|
||||
$sql = 'SELECT t.id, t.gmail_thread_id, t.mailbox, t.subject, t.from_email, t.summary'
|
||||
. $msgCol
|
||||
. ' FROM radar_threads t
|
||||
INNER JOIN radar_thread_skills s ON s.thread_id = t.id AND s.skill = ?
|
||||
WHERE t.is_example = 0
|
||||
AND (t.analysis IS NULL OR t.analysis = \'\')
|
||||
ORDER BY t.received_at DESC, t.id DESC
|
||||
LIMIT ' . (int) $limit;
|
||||
|
||||
return $this->db()->query($sql, ['dossards'])->getResultArray();
|
||||
}
|
||||
|
||||
public function countThreadsNeedingDossardAnalysis(): int
|
||||
{
|
||||
if (! $this->hasAnalysisColumns()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sql = 'SELECT COUNT(*) AS c FROM radar_threads t
|
||||
INNER JOIN radar_thread_skills s ON s.thread_id = t.id AND s.skill = ?
|
||||
WHERE t.is_example = 0
|
||||
AND (t.analysis IS NULL OR t.analysis = \'\')';
|
||||
$row = $this->db()->query($sql, ['dossards'])->getRowArray();
|
||||
|
||||
return (int) ($row['c'] ?? 0);
|
||||
}
|
||||
|
||||
public function listExceptions(): array
|
||||
{
|
||||
return $this->db()->table('radar_exceptions')
|
||||
|
||||
@ -11,6 +11,8 @@ $exceptions = $exceptions ?? [];
|
||||
$threads = $threads ?? [];
|
||||
$feedUrl = $feedUrl ?? site_url('v4/radar/feed');
|
||||
$syncUrl = $syncUrl ?? site_url('v4/radar/sync');
|
||||
$analyzeUrl = $analyzeUrl ?? site_url('v4/radar/analyze');
|
||||
$pendingAi = (int) ($pendingAi ?? 0);
|
||||
$allowlist = $allowlist ?? [];
|
||||
$syncMsg = $syncMsg ?? session()->getFlashdata('radar_sync_msg');
|
||||
|
||||
@ -61,9 +63,15 @@ $statusClass = static function (string $status): string {
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<?php if ($ready): ?>
|
||||
<a href="<?= esc($syncUrl) ?>" class="btn bg-primary text-white">
|
||||
Sync Gmail (v0)
|
||||
</a>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<a href="<?= esc($syncUrl) ?>" class="btn bg-primary text-white">
|
||||
Sync Gmail
|
||||
</a>
|
||||
<a href="<?= esc($analyzeUrl) ?>" class="btn btn-default"
|
||||
title="Peut prendre 1–2 minutes (batch limité)">
|
||||
Analyser dossards<?= $pendingAi > 0 ? ' (' . $pendingAi . ')' : '' ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-zinc-500">Fil mis à jour</p>
|
||||
@ -74,7 +82,7 @@ $statusClass = static function (string $status): string {
|
||||
|
||||
<?php if ($syncMsg): ?>
|
||||
<div class="rounded-xl border border-zinc-300 bg-amber-500/20 px-4 py-3 text-sm font-medium text-zinc-900 dark:border-zinc-600 dark:text-zinc-100">
|
||||
Résultat sync : <?= esc($syncMsg) ?>
|
||||
<?= esc($syncMsg) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -84,15 +92,11 @@ $statusClass = static function (string $status): string {
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-4 text-sm text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<p class="m-0 max-w-2xl">
|
||||
Sync = Gmail readonly sur <span class="font-mono text-xs"><?= esc(implode(', ', $allowlist)) ?></span>.
|
||||
Analyse dossards via couche IA plateforme (Config\Ai).
|
||||
</p>
|
||||
<a href="<?= esc($syncUrl) ?>" class="btn bg-primary text-white shrink-0">
|
||||
Sync Gmail (v0)
|
||||
</a>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
<strong>Sync Gmail</strong> = lecture rapide.
|
||||
<strong>Analyser dossards</strong> = IA (séparé, ne bloque pas le Sync).
|
||||
Mailbox : <span class="font-mono text-xs"><?= esc(implode(', ', $allowlist)) ?></span>.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -277,19 +281,27 @@ $statusClass = static function (string $status): string {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let busy = false;
|
||||
async function tick() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(feedUrl, { credentials: 'same-origin' });
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(function () { ctrl.abort(); }, 8000);
|
||||
const res = await fetch(feedUrl, { credentials: 'same-origin', signal: ctrl.signal });
|
||||
clearTimeout(timer);
|
||||
const data = await res.json();
|
||||
if (clock) clock.textContent = new Date().toLocaleTimeString('fr-CA');
|
||||
if (data && data.ok) render(data.threads || []);
|
||||
} catch (e) {
|
||||
if (clock) clock.textContent = 'hors ligne';
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, 4000);
|
||||
setInterval(tick, 15000);
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
Reference in New Issue
Block a user