mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
V1.15.17 Evolutions des consignes
This commit is contained in:
+3
-2
@@ -105,12 +105,13 @@ if ($agent === '') {
|
||||
// marqué par défaut (ne devrait pas arriver en usage normal, simple
|
||||
// filet de sécurité pour ne jamais laisser CVX_AgentTraitant vide).
|
||||
try {
|
||||
// AGN_Nom renommée AGN_Prenom en base (7 septembre 2026, nuit).
|
||||
$stmtDefaut = $pdo->query(
|
||||
"SELECT AGN_Nom FROM ASP_Agents WHERE AGN_Defaut = 'O' ORDER BY AGN_Incr ASC LIMIT 1"
|
||||
"SELECT AGN_Prenom FROM ASP_Agents WHERE AGN_Defaut = 'O' ORDER BY AGN_Incr ASC LIMIT 1"
|
||||
);
|
||||
$agent = $stmtDefaut->fetchColumn();
|
||||
if ($agent === false || $agent === '') {
|
||||
$stmtPremier = $pdo->query('SELECT AGN_Nom FROM ASP_Agents ORDER BY AGN_Incr ASC LIMIT 1');
|
||||
$stmtPremier = $pdo->query('SELECT AGN_Prenom FROM ASP_Agents ORDER BY AGN_Incr ASC LIMIT 1');
|
||||
$agent = $stmtPremier->fetchColumn();
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
|
||||
+8
-1
@@ -45,7 +45,14 @@ try {
|
||||
// Loulou"), utilisées par micro.js pour tolérer les confusions
|
||||
// récurrentes de la reconnaissance vocale sans avoir à modifier le
|
||||
// code à chaque fois — colonne éditable directement par Jean-Marc.
|
||||
$stmt = $pdo->query('SELECT AGN_Incr, AGN_Nom, AGN_Fonction, AGN_Variantes FROM ASP_Agents ORDER BY AGN_Incr ASC');
|
||||
//
|
||||
// AGN_Nom renommée AGN_Prenom (7 septembre 2026, nuit — Jean-Marc,
|
||||
// homogénéisation avec USR_Prenom) : répercuté ici et dans tous les
|
||||
// consommateurs de ce endpoint (micro.js, contact_ia_panel.js,
|
||||
// contact_ia_interprete.php) — la clé JSON change de nom avec la colonne,
|
||||
// pas d'alias de compatibilité gardé, pour ne pas laisser deux noms
|
||||
// cohabiter pour la même chose.
|
||||
$stmt = $pdo->query('SELECT AGN_Incr, AGN_Prenom, AGN_Fonction, AGN_Variantes FROM ASP_Agents ORDER BY AGN_Incr ASC');
|
||||
echo json_encode(['agents' => $stmt->fetchAll()]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
|
||||
@@ -117,14 +117,28 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmtNom = $pdoCible->prepare('SELECT USR_Nom FROM ASP_Users WHERE USR_Code = ?');
|
||||
// USR_Prenom récupéré ici en même temps que USR_Nom (7 septembre 2026, nuit)
|
||||
// et posé en session : sert désormais de "nom affiché" pour les réalisations
|
||||
// créées par cet utilisateur (voir REA_NomAffiche, realisations.php) sans
|
||||
// jamais toucher à USR_Code lui-même — Jean-Marc a explicitement écarté un
|
||||
// renommage de USR_Code (trop structurant) et une jointure ASP_Users refaite
|
||||
// à chaque affichage/écriture (source de complication inutile, et
|
||||
// potentiellement impossible si utilisateurs et agents IA finissent un jour
|
||||
// dans des bases séparées) : un seul aller-retour ici, à la connexion,
|
||||
// suffit pour tout le reste de la session.
|
||||
$stmtNom = $pdoCible->prepare('SELECT USR_Nom, USR_Prenom FROM ASP_Users WHERE USR_Code = ?');
|
||||
$stmtNom->execute([$routage['MIR_Code']]);
|
||||
$nom = $stmtNom->fetchColumn();
|
||||
$ligneUsr = $stmtNom->fetch();
|
||||
$nom = $ligneUsr['USR_Nom'] ?? null;
|
||||
$prenom = $ligneUsr['USR_Prenom'] ?? null;
|
||||
|
||||
$_SESSION['USR_Prenom'] = $prenom;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'user' => [
|
||||
'USR_Code' => $routage['MIR_Code'],
|
||||
'USR_Nom' => $nom !== false ? $nom : null,
|
||||
'USR_Nom' => $nom,
|
||||
'USR_Prenom' => $prenom,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* ASPIC — consignes_apercu.php (7 septembre 2026, soir)
|
||||
*
|
||||
* Endpoint dédié au badge IA générique (maquette fournie par Jean-Marc, voir
|
||||
* journal) : répond très vite, SANS jamais appeler le moindre service
|
||||
* externe, à la question « si j'enregistrais ces valeurs maintenant, au
|
||||
* moins une consigne de Traitement IA se déclencherait-elle ? ».
|
||||
*
|
||||
* Existe pour une seule raison : ne JAMAIS deviner ni recopier en dur, côté
|
||||
* écran, la condition qui déclenche une consigne IA (voir les échanges du
|
||||
* 7 septembre 2026 dans le journal — Jean-Marc a refusé un premier essai qui
|
||||
* testait `ville renseignée et CP vide` directement en JavaScript). La seule
|
||||
* condition qui compte est celle écrite dans le jumeau
|
||||
* "<fonction>_seraitDeclenchee" de la fonction elle-même (voir en-tête de
|
||||
* consignes_fonctions.php) ; cet endpoint se contente d'interroger
|
||||
* auMoinsUneConsigneIaSeraitDeclenchee() (moteur_consignes.php), qui parcourt
|
||||
* les consignes actives et appelle ces jumeaux — rien d'autre.
|
||||
*
|
||||
* Le front-end (liste_contacts.html, validerFormulaireContact()) appelle cet
|
||||
* endpoint AVANT l'enregistrement réel (POST/PUT vers contacts.php), pour
|
||||
* savoir s'il doit afficher le badge IA pendant l'attente de la réponse.
|
||||
* Un enregistrement qui ne déclenche aucune IA reste donc instantané à
|
||||
* l'écran, sans badge — exigence explicite de Jean-Marc ("hors de question
|
||||
* d'afficher systématiquement le badge IA, alors qu'il n'y est pas fait
|
||||
* appel").
|
||||
*
|
||||
* Entrée JSON : { "contexte": "Contacts", "portee": "Création"|"Modification",
|
||||
* "donnees": { ...valeurs actuelles du formulaire... } }
|
||||
* Sortie JSON : { "declenche": true|false }
|
||||
*
|
||||
* Accès réservé aux utilisateurs identifiés, même principe que contacts.php.
|
||||
* Toujours POST (l'objet "donnees" peut contenir des champs qui n'ont pas
|
||||
* leur place dans une query string). Ne touche jamais à $_SESSION au-delà de
|
||||
* la vérification d'identification, et n'écrit jamais en base.
|
||||
*/
|
||||
|
||||
session_start();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require __DIR__ . '/db.php';
|
||||
require __DIR__ . '/consignes_fonctions.php';
|
||||
require __DIR__ . '/moteur_consignes.php';
|
||||
|
||||
// Accès réservé aux utilisateurs identifiés (session ouverte par aspic.php).
|
||||
if (empty($_SESSION['USR_Code'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$corps = json_decode(file_get_contents('php://input'), true);
|
||||
if (!is_array($corps)) {
|
||||
$corps = [];
|
||||
}
|
||||
|
||||
$contexte = trim((string)($corps['contexte'] ?? ''));
|
||||
$portee = trim((string)($corps['portee'] ?? ''));
|
||||
$donnees = is_array($corps['donnees'] ?? null) ? $corps['donnees'] : [];
|
||||
|
||||
if ($contexte === '' || $portee === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Paramètres "contexte" et "portee" obligatoires']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Connexion base de données impossible']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$declenche = auMoinsUneConsigneIaSeraitDeclenchee($pdo, $contexte, $portee, $donnees);
|
||||
} catch (Throwable $e) {
|
||||
// Repli prudent : en cas de souci imprévu, on répond "pas de badge" plutôt
|
||||
// que de faire échouer l'écran pour un simple indicateur visuel — même
|
||||
// philosophie de robustesse que les fonctions IA elles-mêmes (voir
|
||||
// consignes_fonctions.php).
|
||||
$declenche = false;
|
||||
}
|
||||
|
||||
echo json_encode(['declenche' => $declenche]);
|
||||
+242
-8
@@ -10,14 +10,39 @@
|
||||
* contacts.php depuis la version du 6 septembre 2026 (soir).
|
||||
*
|
||||
* Convention pour écrire une nouvelle fonction ici :
|
||||
* - Traitement : signature (string $valeur): string — reçoit la valeur
|
||||
* actuelle du champ, retourne la valeur transformée.
|
||||
* - Test : signature (string $valeur): bool — retourne vrai si la valeur
|
||||
* respecte la consigne, faux sinon (le message affiché à l'échec vient de
|
||||
* CNS_MessageErreur en base, jamais de cette fonction).
|
||||
* Fichier volontairement sans aucune dépendance (pas de session, pas de
|
||||
* base de données) : chaque fonction ne fait que transformer ou tester une
|
||||
* valeur qu'on lui passe.
|
||||
* - Traitement à UN champ : signature (string $valeur): string — reçoit la
|
||||
* valeur actuelle du champ, retourne la valeur transformée.
|
||||
* - Test à UN champ : signature (string $valeur): bool — retourne vrai si la
|
||||
* valeur respecte la consigne, faux sinon (le message affiché à l'échec
|
||||
* vient de CNS_MessageErreur en base, jamais de cette fonction).
|
||||
* - Traitement/Test à PLUSIEURS champs (extension du 7 septembre 2026, voir
|
||||
* moteur_consignes.php) : quand CNS_Champ contient plusieurs noms de champs
|
||||
* séparés par une virgule, la fonction reçoit un tableau associatif
|
||||
* [nom_du_champ => valeur] au lieu d'une simple chaîne — un Traitement
|
||||
* retourne alors ce même type de tableau (valeurs mises à jour, mêmes clés),
|
||||
* un Test retourne toujours un simple bool. Voir
|
||||
* consigne_completer_code_postal() ci-dessous pour un exemple.
|
||||
* - Jumeau "_seraitDeclenchee" (7 septembre 2026, soir) : toute fonction de
|
||||
* Traitement qui interroge un service externe (IA) et porte
|
||||
* FON_VisuelAttenteIA = 'O' DOIT avoir, en plus, une fonction nommée
|
||||
* "<nom_de_la_fonction>_seraitDeclenchee", signature (array $valeurs): bool
|
||||
* — répond "est-ce que j'agirais sur ces valeurs", sans jamais rien faire
|
||||
* d'autre (aucun appel réseau). La vraie fonction appelle ce jumeau EN
|
||||
* PREMIER (voir consigne_completer_code_postal() ci-dessous) : la condition
|
||||
* de déclenchement n'existe ainsi qu'à un seul endroit, jamais recopiée ni
|
||||
* devinée ailleurs (pas même dans ce fichier) — c'est ce jumeau
|
||||
* qu'interroge consignes_apercu.php pour piloter le badge IA générique côté
|
||||
* écran, voir ce fichier et moteur_consignes.php
|
||||
* (auMoinsUneConsigneIaSeraitDeclenchee()).
|
||||
* Fichier volontairement sans dépendance à la session ni à la base de
|
||||
* données : chaque fonction ne fait que transformer ou tester une ou
|
||||
* plusieurs valeurs qu'on lui passe. Exception depuis le 7 septembre 2026
|
||||
* (soir) : une fonction peut interroger un service externe (API d'IA, voir
|
||||
* consigne_completer_code_postal()) quand la consigne elle-même l'exige
|
||||
* explicitement — jamais la session ni la base ASPIC, seulement un appel
|
||||
* réseau sortant, et toujours avec un repli silencieux (valeurs inchangées)
|
||||
* en cas d'échec : un Traitement ne doit jamais faire échouer un
|
||||
* enregistrement à cause d'un service tiers indisponible.
|
||||
*/
|
||||
|
||||
// ASP_Fonctions.FON_Nom = 'MAJUSCULE' (Nature=Traitement)
|
||||
@@ -55,3 +80,212 @@ function consigne_premiere_lettre_majuscule(string $valeur): string
|
||||
$valeur
|
||||
);
|
||||
}
|
||||
|
||||
// ASP_Fonctions.FON_Nom = 'MAIL_VALIDE' (Nature=Test), 7 septembre 2026.
|
||||
// Vrai si la valeur ressemble à une adresse mail syntaxiquement correcte,
|
||||
// faux sinon — une simple cohérence de saisie ("@" présent, domaine
|
||||
// plausible), jamais une vérification que la boîte existe réellement.
|
||||
// Champ facultatif : une valeur vide est considérée conforme ici (le
|
||||
// caractère obligatoire ou non du champ reste la responsabilité d'une
|
||||
// consigne NON_VIDE séparée, combinable avec celle-ci sur le même champ).
|
||||
//
|
||||
// Volontairement PAS de regex écrite à la main du style "@ suivi de deux
|
||||
// groupes de lettres séparés par un point" : une adresse valide peut avoir
|
||||
// un domaine à plus de deux niveaux (ex. toto@tata.ndd.com), un chiffre, un
|
||||
// tiret... filter_var(FILTER_VALIDATE_EMAIL) est le validateur natif de PHP,
|
||||
// déjà utilisé ailleurs dans ASPIC pour la même vérification (voir
|
||||
// prospects_core.php, contrôle sur PSP_Mail) — le reprendre ici garde une
|
||||
// seule et même définition de "adresse mail valide" dans toute l'application
|
||||
// plutôt que deux logiques différentes qui pourraient diverger.
|
||||
function consigne_mail_valide(string $valeur): bool
|
||||
{
|
||||
$valeur = trim($valeur);
|
||||
if ($valeur === '') return true;
|
||||
return filter_var($valeur, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
// ASP_Fonctions.FON_Nom = 'SANS_CHIFFRE' (Nature=Test), 7 septembre 2026.
|
||||
// Vrai si la valeur ne contient aucun chiffre, faux sinon — champ facultatif
|
||||
// (une valeur vide est conforme, même principe que consigne_mail_valide() :
|
||||
// le caractère obligatoire est la responsabilité d'une consigne NON_VIDE
|
||||
// séparée, combinable sur le même champ). Générique et réutilisable tel
|
||||
// quel sur n'importe quel champ texte : Jean-Marc l'a demandée pour Nom,
|
||||
// Prénom et Ville, ce qui ne demande ici AUCUNE fonction par champ — juste
|
||||
// trois lignes dans ASP_Consignes (CNT_Nom, CNT_Prenom, CNT_AdrVille)
|
||||
// pointant toutes vers cette même fonction, chacune avec son propre
|
||||
// CNS_MessageErreur si on veut un message différent par champ.
|
||||
function consigne_sans_chiffre(string $valeur): bool
|
||||
{
|
||||
return trim($valeur) === '' || !preg_match('/\d/', $valeur);
|
||||
}
|
||||
|
||||
// ASP_Fonctions.FON_Nom = 'CP_DEPUIS_VILLE' (Nature=Traitement, CNS_Champ =
|
||||
// "CNT_AdrVille,CNT_AdrCP"), 7 septembre 2026 (soir).
|
||||
// Première consigne "qui va chercher sa réponse avec l'IA" (demande de
|
||||
// Jean-Marc) : complète le code postal à partir de la ville quand celui-ci
|
||||
// n'a pas été saisi. Un champ seul ne suffisait pas ici (il faut la ville en
|
||||
// entrée pour produire le code postal en sortie) — voir l'extension du
|
||||
// moteur à plusieurs champs dans moteur_consignes.php.
|
||||
//
|
||||
// Ne fait RIEN (retourne les valeurs inchangées) si le code postal est déjà
|
||||
// renseigné — jamais d'écrasement d'une saisie existante, même douteuse —
|
||||
// ou si la ville est vide (rien à déduire). Sinon interroge le même modèle
|
||||
// que contact_ia_interprete.php (OPENAI_API_KEY, voir ce fichier pour le
|
||||
// détail de l'intégration) avec une sortie structurée stricte : le modèle
|
||||
// renvoie soit un code postal à 5 chiffres qu'il connaît avec une confiance
|
||||
// raisonnable, soit null s'il n'est pas sûr — jamais un code inventé, même
|
||||
// principe de prudence que consigne_mail_valide() ci-dessus (mieux vaut
|
||||
// laisser le champ vide qu'afficher une fausse information). Une ville qui
|
||||
// couvre plusieurs codes postaux sans qu'aucun ne soit clairement principal
|
||||
// (ex. arrondissements de Paris/Lyon/Marseille) doit renvoyer null plutôt que
|
||||
// de deviner — c'est écrit explicitement dans la consigne envoyée au modèle.
|
||||
//
|
||||
// Toute défaillance (clé absente, réseau, quota, réponse inattendue) se
|
||||
// traduit par un retour silencieux des valeurs inchangées : un Traitement ne
|
||||
// doit jamais bloquer l'enregistrement d'un contact à cause d'un service
|
||||
// externe (voir commentaire d'en-tête de moteur_consignes.php) — contrairement
|
||||
// à un Test, dont l'échec est lui affiché à l'utilisateur.
|
||||
// Jumeau "_seraitDeclenchee" (voir en-tête de fichier) : réplique à elle
|
||||
// seule la condition de déclenchement de consigne_completer_code_postal()
|
||||
// ci-dessous, sans jamais faire le moindre appel réseau. C'est la SEULE
|
||||
// fonction que consignes_apercu.php interroge pour savoir si le badge IA doit
|
||||
// apparaître — la vraie fonction l'appelle elle-même en premier, pour que
|
||||
// cette condition n'existe jamais qu'à cet unique endroit.
|
||||
function consigne_completer_code_postal_seraitDeclenchee(array $valeurs): bool
|
||||
{
|
||||
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
||||
$codePostal = trim((string)($valeurs['CNT_AdrCP'] ?? ''));
|
||||
|
||||
return $codePostal === '' && $ville !== '';
|
||||
}
|
||||
|
||||
function consigne_completer_code_postal(array $valeurs): array
|
||||
{
|
||||
if (!consigne_completer_code_postal_seraitDeclenchee($valeurs)) {
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
||||
|
||||
$apiKey = trim((string) getenv('OPENAI_API_KEY'));
|
||||
if (!$apiKey) {
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
$schema = [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'code_postal' => [
|
||||
'type' => ['string', 'null'],
|
||||
'description' => "Code postal français à 5 chiffres correspondant à la ville donnée, uniquement si connu avec une confiance raisonnable. null si la ville est ambiguë, inconnue, introuvable en France, ou couvre plusieurs codes postaux sans qu'un seul soit clairement le plus probable.",
|
||||
],
|
||||
],
|
||||
'required' => ['code_postal'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
|
||||
$corpsRequete = [
|
||||
'model' => 'gpt-5.6-luna', // même modèle que contact_ia_interprete.php, voir ce fichier.
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => "Tu donnes le code postal français d'une ville, pour un CRM. "
|
||||
. "N'invente jamais un code postal : renvoie null si tu n'es pas "
|
||||
. "raisonnablement sûr, si la ville est ambiguë ou inconnue, ou si "
|
||||
. "elle est à cheval sur plusieurs codes postaux sans qu'un seul soit "
|
||||
. "clairement le principal (ex. grandes villes découpées en "
|
||||
. "arrondissements).",
|
||||
],
|
||||
['role' => 'user', 'content' => $ville],
|
||||
],
|
||||
'response_format' => [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => ['name' => 'code_postal_ville', 'strict' => true, 'schema' => $schema],
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($corpsRequete),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $apiKey,
|
||||
],
|
||||
// Appelée en synchrone pendant l'enregistrement d'un contact (choix
|
||||
// de Jean-Marc, 7 septembre 2026 : "oui, tel quel" malgré le coût/la
|
||||
// latence) : délai volontairement court, on préfère abandonner
|
||||
// (valeurs inchangées) plutôt que de faire attendre longtemps sur un
|
||||
// enrichissement facultatif.
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
]);
|
||||
$reponseBrute = curl_exec($ch);
|
||||
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($reponseBrute === false || $codeHttp !== 200) {
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
$reponseDecodee = json_decode($reponseBrute, true);
|
||||
$contenuJson = $reponseDecodee['choices'][0]['message']['content'] ?? null;
|
||||
$extraction = $contenuJson ? json_decode($contenuJson, true) : null;
|
||||
$codePostalDeduit = $extraction['code_postal'] ?? null;
|
||||
|
||||
// Garde-fou défensif (même principe qu'ailleurs dans ASPIC : jamais
|
||||
// confiance aveugle en un service externe) : un code postal français est
|
||||
// exactement 5 chiffres, sinon la réponse est ignorée.
|
||||
if (is_string($codePostalDeduit) && preg_match('/^\d{5}$/', $codePostalDeduit)) {
|
||||
$valeurs['CNT_AdrCP'] = $codePostalDeduit;
|
||||
// Journalisation (7 septembre 2026, soir, demande de Jean-Marc après
|
||||
// son premier essai réel — "indispensable" pour pouvoir relier une
|
||||
// ligne de coût IA à sa cause) : clé réservée "_realisation", voir
|
||||
// moteur_consignes.php pour le mécanisme générique. Pas de champ
|
||||
// 'agent' ici (contrairement au tout premier jet, qui inventait "Mat"
|
||||
// en dur) : Jean-Marc a fait remarquer, à raison, qu'ASP_Consignes
|
||||
// porte déjà CNS_Agent — c'est le moteur (executerTraitements(), voir
|
||||
// moteur_consignes.php) qui l'y lit et l'attribue à la réalisation,
|
||||
// jamais cette fonction. ACT_Code 'IA_CP' à créer à la main dans
|
||||
// ASP_Actions (table gérée à la main, voir realisations.php) avant le
|
||||
// premier passage réel en production.
|
||||
$valeurs['_realisation'] = [
|
||||
'action' => 'IA_CP',
|
||||
'commentaire' => "Code postal $codePostalDeduit déduit de la ville « $ville » par l'IA.",
|
||||
];
|
||||
} else {
|
||||
// Appel IA fait, coût réellement engagé, mais sans réponse
|
||||
// exploitable — ex. Paris/Lyon/Marseille, découpées en
|
||||
// arrondissements sans code postal unique, pour lesquelles le modèle
|
||||
// renvoie sciemment null plutôt que de deviner (voir le prompt
|
||||
// système ci-dessus). Repéré par Jean-Marc (7 septembre 2026, nuit,
|
||||
// test sur "Paris") : "il y a un appel fait à l'IA et pas de trace
|
||||
// dans les réalisations, ce qui n'est pas normal" — un appel qui ne
|
||||
// trouve rien coûte quand même des tokens, il doit donc apparaître
|
||||
// au même titre qu'un appel réussi. Même ACT_Code 'IA_CP', un
|
||||
// commentaire différent suffit à distinguer les deux cas dans
|
||||
// l'historique, sans demander à Jean-Marc de créer un deuxième code
|
||||
// dans ASP_Actions.
|
||||
$valeurs['_realisation'] = [
|
||||
'action' => 'IA_CP',
|
||||
'commentaire' => "Aucun code postal déduit pour la ville « $ville » (ambiguë ou inconnue pour l'IA).",
|
||||
];
|
||||
// '_avertissement' (nouvelle clé réservée, voir moteur_consignes.php) :
|
||||
// contrairement à '_realisation' (historique consulté a posteriori),
|
||||
// ce message est affiché IMMÉDIATEMENT à l'écran — sans lui,
|
||||
// l'utilisateur ne voit rien du tout et ne sait pas que l'IA a bien
|
||||
// réfléchi puis sciemment renoncé, plutôt qu'un simple silence.
|
||||
// Message volontairement impératif (7 septembre 2026, demande de
|
||||
// Jean-Marc) : le toast qui l'affiche reste à l'écran tant que
|
||||
// l'utilisateur n'a pas cliqué pour le refermer (voir
|
||||
// liste_contacts.html/afficherToast()), l'instruction doit donc être
|
||||
// sans ambiguïté sur ce qu'il reste à faire.
|
||||
$valeurs['_avertissement'] =
|
||||
"Code postal non déterminé pour « $ville » (ville ambiguë ou "
|
||||
. "avec plusieurs codes postaux possibles). Saisissez le code "
|
||||
. "postal vous-même.";
|
||||
}
|
||||
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ try {
|
||||
// fichier reste volontairement passif : il ne renvoie que ce qui a été
|
||||
// réellement dit (assistant_id reste null si personne n'est cité), sans
|
||||
// jamais anticiper une décision qui appartient à l'étage suivant.
|
||||
$agents = $pdo->query('SELECT AGN_Incr, AGN_Nom, AGN_Fonction, AGN_Variantes FROM ASP_Agents ORDER BY AGN_Incr ASC')->fetchAll();
|
||||
// AGN_Nom renommée AGN_Prenom en base (7 septembre 2026, nuit).
|
||||
$agents = $pdo->query('SELECT AGN_Incr, AGN_Prenom, AGN_Fonction, AGN_Variantes FROM ASP_Agents ORDER BY AGN_Incr ASC')->fetchAll();
|
||||
$actionsDisponibles = $pdo->query('SELECT ACT_Code, ACT_Libelle FROM ASP_Actions ORDER BY ACT_Libelle ASC')->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
@@ -226,7 +227,7 @@ $listeProfessions = $professions
|
||||
: '(aucune profession existante)';
|
||||
$listeAgents = $agents
|
||||
? implode(', ', array_map(
|
||||
fn($a) => $a['AGN_Incr'] . ' = ' . $a['AGN_Nom'] .
|
||||
fn($a) => $a['AGN_Incr'] . ' = ' . $a['AGN_Prenom'] .
|
||||
($a['AGN_Variantes'] ? ' (variantes tolérées : ' . $a['AGN_Variantes'] . ')' : ''),
|
||||
$agents
|
||||
))
|
||||
|
||||
+3
-1
@@ -554,7 +554,9 @@
|
||||
(agents || []).forEach(function(a){
|
||||
const option = document.createElement("option");
|
||||
option.value = String(a.AGN_Incr);
|
||||
option.textContent = a.AGN_Nom + (a.AGN_Fonction ? " (" + a.AGN_Fonction + ")" : "");
|
||||
// AGN_Nom renommée AGN_Prenom en base (7 septembre 2026, nuit) — voir
|
||||
// agents.php, qui renvoie désormais cette clé sous son nouveau nom.
|
||||
option.textContent = a.AGN_Prenom + (a.AGN_Fonction ? " (" + a.AGN_Fonction + ")" : "");
|
||||
selectAssistant.appendChild(option);
|
||||
});
|
||||
if(!agents || !agents.length){
|
||||
|
||||
+73
-6
@@ -69,6 +69,23 @@
|
||||
* pas de distinction Saisie/Validation (tout s'applique à l'enregistrement),
|
||||
* pas de vérification en temps réel côté écran pour l'instant — décision de
|
||||
* Jean-Marc du 6 septembre 2026, priorité à la robustesse.
|
||||
*
|
||||
* Journalisation des Traitements IA dans ASP_Realisations (7 septembre 2026,
|
||||
* soir) : appliquerConsignes() peut remonter, via son paramètre par référence
|
||||
* $journalisationsIA, une ou plusieurs entrées à journaliser (ex. code postal
|
||||
* déduit par l'IA depuis la ville — voir consigne_completer_code_postal()).
|
||||
* Écrites ICI, directement en base, plutôt que laissées au front-end comme
|
||||
* CNT_CRE/CNT_MAJ (voir liste_contacts.html, validerFormulaireContact()) :
|
||||
* REA_CodeUser/REA_NomAffiche reçoivent alors le nom d'agent IA (ex. "Matilda"
|
||||
* — voir ASP_Agents.AGN_Prenom, et CNS_Agent sur la consigne qui a déclenché le
|
||||
* Traitement), pas
|
||||
* l'utilisateur humain de la session — hors de portée de realisations.php,
|
||||
* qui impose volontairement $_SESSION['USR_Code'] (correctif de sécurité du
|
||||
* 24 août 2026, voir ce fichier) et ne doit surtout pas rouvrir la
|
||||
* possibilité qu'un client falsifie REA_CodeUser. Best-effort (try/catch
|
||||
* séparé) : un souci de journalisation ne doit jamais faire échouer
|
||||
* l'enregistrement du contact lui-même, même principe que la journalisation
|
||||
* CNT_CRE/CNT_MAJ côté front.
|
||||
*/
|
||||
|
||||
session_start();
|
||||
@@ -122,6 +139,43 @@ function construireMotifLike(string $valeur): array
|
||||
return [$motif, '\\'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Journalise dans ASP_Realisations les Traitements IA remontés par
|
||||
* appliquerConsignes() (voir moteur_consignes.php et le commentaire d'en-tête
|
||||
* de ce fichier) — ex. code postal déduit de la ville. Best-effort : une
|
||||
* exception ici est avalée (juste consignée au log serveur), jamais renvoyée
|
||||
* au client — un souci de journalisation ne doit pas laisser croire que
|
||||
* l'enregistrement du contact a échoué alors qu'il a réussi.
|
||||
*/
|
||||
function journaliserRealisationsIA(PDO $pdo, int $idContact, array $journalisationsIA): void
|
||||
{
|
||||
if (!$journalisationsIA) return;
|
||||
try {
|
||||
// REA_CodeUser ET REA_NomAffiche reçoivent la même valeur pour un
|
||||
// agent IA (7 septembre 2026, nuit — "le prénom de l'agent en dur",
|
||||
// Jean-Marc) : contrairement à un utilisateur humain, un agent n'a
|
||||
// pas de code technique court séparé de son nom affiché — la valeur
|
||||
// vient déjà en clair de CNS_Agent (voir moteur_consignes.php),
|
||||
// aucune recherche supplémentaire à faire ici.
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_NomAffiche, REA_Date, REA_Action, REA_Comment)
|
||||
VALUES (?, ?, ?, NOW(), ?, ?)'
|
||||
);
|
||||
foreach ($journalisationsIA as $j) {
|
||||
$nomAgent = (string)($j['agent'] ?? 'IA');
|
||||
$stmt->execute([
|
||||
$idContact,
|
||||
$nomAgent,
|
||||
$nomAgent,
|
||||
(string)($j['action'] ?? 'IA'),
|
||||
(string)($j['commentaire'] ?? ''),
|
||||
]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('ASPIC contacts: échec journalisation ASP_Realisations (Traitement IA) : ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
@@ -199,10 +253,14 @@ try {
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Création', $data);
|
||||
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Création', $data, $agentConsigne, $journalisationsIA, $avertissementsIA);
|
||||
if ($erreurConsigne !== null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => $erreurConsigne]);
|
||||
// 'agent' (CNS_Agent, 7 septembre 2026) : vaut JSON null si la
|
||||
// consigne en échec n'en a pas — sans effet côté front, où
|
||||
// afficherToast n'affiche la ligne agent que si la valeur est
|
||||
// présente et non vide.
|
||||
echo json_encode(['error' => $erreurConsigne, 'agent' => $agentConsigne]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -245,7 +303,14 @@ try {
|
||||
$stmtCode = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Code = ? WHERE CNT_Incr = ?');
|
||||
$stmtCode->execute([$code, $nouvelIncr]);
|
||||
|
||||
echo json_encode(['id' => $nouvelIncr, 'code' => $code]);
|
||||
journaliserRealisationsIA($pdo, $nouvelIncr, $journalisationsIA);
|
||||
|
||||
// 'avertissements' (7 septembre 2026, nuit) : messages à afficher
|
||||
// immédiatement à l'écran, voir moteur_consignes.php ("_avertissement")
|
||||
// — distinct de la journalisation ci-dessus, qui va dans
|
||||
// l'historique mais ne s'affiche pas tout de suite. Tableau vide
|
||||
// la plupart du temps, le front-end n'affiche alors rien.
|
||||
echo json_encode(['id' => $nouvelIncr, 'code' => $code, 'avertissements' => $avertissementsIA]);
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
@@ -256,10 +321,10 @@ try {
|
||||
}
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Modification', $data);
|
||||
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Modification', $data, $agentConsigne, $journalisationsIA, $avertissementsIA);
|
||||
if ($erreurConsigne !== null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => $erreurConsigne]);
|
||||
echo json_encode(['error' => $erreurConsigne, 'agent' => $agentConsigne]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -295,7 +360,9 @@ try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
journaliserRealisationsIA($pdo, (int)$_GET['id'], $journalisationsIA);
|
||||
|
||||
echo json_encode(['success' => true, 'avertissements' => $avertissementsIA]);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
|
||||
+261
-48
@@ -48,16 +48,23 @@
|
||||
background:#ad580d url('ecran_fond.jpg') center center / contain no-repeat;
|
||||
position:relative;
|
||||
}
|
||||
/* Étiquette "Contacts" sur la tranche du 1er classeur (2 septembre 2026,
|
||||
essai). Positionnée en JS plutôt qu'en CSS pur : comme l'image est en
|
||||
"contain", sa taille et sa position réelles à l'écran changent avec la
|
||||
proportion de la fenêtre — même calcul que "background-size:contain"
|
||||
refait à la main pour caler ce texte sur l'étiquette blanche, quelle
|
||||
que soit la fenêtre. Coordonnées mesurées sur ecran_fond.jpg (image de
|
||||
référence 1280×650) : rectangle d'étiquette à peu près x:[54,91],
|
||||
y:[376,481] sur cette image. rotate(-90deg) : lecture bas → haut,
|
||||
comme demandé (on tourne la tête pour lire), plutôt que haut → bas. */
|
||||
#etiquette-contacts{
|
||||
/* Étiquettes des 9 classeurs (7 septembre 2026 — évolution de l'essai à
|
||||
un seul classeur "Contacts" du 2 septembre). Nouvelle photo fournie par
|
||||
Jean-Marc, mêmes dimensions de référence (1024×457) et même bureau/mur/
|
||||
micro (vérifié par comparaison pixel à pixel avec l'ancienne image :
|
||||
seule la zone des classeurs, x≈[40,400], diffère réellement — le reste
|
||||
n'est que du bruit de compression) — les constantes MICRO_ZONE, ICONE et
|
||||
ZONE_POSTITS restent donc inchangées plus bas, seules les constantes de
|
||||
classeurs et la pastille "Contacts" ont été remesurées sur la nouvelle
|
||||
photo.
|
||||
Toujours positionnées en JS (voir creerClasseurs()/positionner()) pour
|
||||
le même motif que le 2 septembre : l'image étant en "contain", sa
|
||||
taille/position réelles à l'écran changent avec la proportion de la
|
||||
fenêtre. rotate(-90deg) : lecture bas → haut (on tourne la tête pour
|
||||
lire), inchangé. Règles génériques (classe, plus un seul id) : il y a
|
||||
désormais 9 étiquettes/classeurs au lieu d'un seul — voir le tableau
|
||||
CLASSEURS dans le script. */
|
||||
.etiquette-classeur{
|
||||
position:fixed;
|
||||
transform:translate(-50%,-50%) rotate(-90deg);
|
||||
transform-origin:center;
|
||||
@@ -69,18 +76,25 @@
|
||||
pointer-events:none;
|
||||
user-select:none;
|
||||
}
|
||||
/* Zone cliquable calée sur la tranche visible (face plate, sans le
|
||||
dessus incliné) du 1er classeur — même principe de positionnement en
|
||||
JS que l'étiquette ci-dessus. Pas d'habillage visuel par défaut (le
|
||||
classeur photographié suffit), juste un léger éclaircissement au survol
|
||||
pour signaler que c'est cliquable, en plus du curseur et de l'infobulle. */
|
||||
#classeur-contacts{
|
||||
/* Zone calée sur la tranche visible (face plate, sans le dessus incliné)
|
||||
de chaque classeur — même principe de positionnement en JS que
|
||||
l'étiquette ci-dessus. Pas d'habillage visuel par défaut (le classeur
|
||||
photographié suffit), juste un léger éclaircissement au survol pour
|
||||
signaler que c'est cliquable, en plus du curseur et de l'infobulle —
|
||||
UNIQUEMENT sur le classeur réellement branché (voir .classeur-actif
|
||||
ci-dessous) : les 8 autres n'ont pas encore d'écran derrière (voir
|
||||
tableau CLASSEURS), pas de survol trompeur pour eux tant que ce n'est
|
||||
pas le cas.*/
|
||||
.classeur-zone{
|
||||
position:fixed;
|
||||
display:block;
|
||||
}
|
||||
.classeur-zone.classeur-actif{
|
||||
cursor:pointer;
|
||||
box-shadow:inset 0 0 0 0 rgba(255,255,255,0);
|
||||
transition:box-shadow .12s ease;
|
||||
}
|
||||
#classeur-contacts:hover{
|
||||
.classeur-zone.classeur-actif:hover{
|
||||
box-shadow:inset 0 0 0 2px rgba(255,255,255,.7);
|
||||
}
|
||||
/* Chevalet supprimé le 3 septembre 2026 (voir journal) : Jean-Marc l'a
|
||||
@@ -348,8 +362,10 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a id="classeur-contacts" href="liste_contacts.html" title="Ouvrir le classeur des contacts"></a>
|
||||
<div id="etiquette-contacts">Contacts</div>
|
||||
<!-- Les 9 zones cliquables + étiquettes de classeurs (7 septembre 2026) sont
|
||||
créées par creerClasseurs() (voir script) à partir du tableau CLASSEURS,
|
||||
sur le même principe que les post-its plus bas : éviter de dupliquer 9
|
||||
fois à la main un bloc HTML quasi identique. -->
|
||||
<button id="micro-desk" type="button" title="Le micro est inactif"></button>
|
||||
<div id="micro-icone">
|
||||
<svg id="glyphe-repos" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -389,28 +405,139 @@ Créé par Mevelia Solutions">
|
||||
// silhouette du micro) : décalage vertical uniforme de +13 confirmé pour
|
||||
// tout ce qui était calé sur le bureau ou le mur, aucun décalage
|
||||
// horizontal — cohérent avec la description de Jean-Marc.
|
||||
// 4ᵉ version le 7 septembre 2026 : Jean-Marc a retravaillé l'image pour
|
||||
// passer d'un unique classeur "Contacts" à une rangée de 9 classeurs
|
||||
// (mêmes dimensions 1024×457, même bureau/mur/micro/carnet/téléphone,
|
||||
// vérifié par différence pixel à pixel entre les deux photos — seule la
|
||||
// zone des classeurs, x≈[40,400], a réellement changé). Constantes de
|
||||
// classeurs et pastille "Contacts" remesurées ci-dessous sur cette
|
||||
// nouvelle version ; MICRO_ZONE_*/ICONE_*/ZONE_POSTITS_* inchangées.
|
||||
const IMG_W = 1024, IMG_H = 457;
|
||||
// Centre de l'étiquette du 1er classeur (57.5, 282 -> 57.5, 295 : +13).
|
||||
const ETIQ_CENTRE_X_FRAC = 57.5 / IMG_W;
|
||||
const ETIQ_CENTRE_Y_FRAC = 295 / IMG_H;
|
||||
const ETIQ_POLICE_FRAC = 19 / IMG_W;
|
||||
// Rectangle de la tranche visible du 1er classeur (x inchangé, y:[224,397]
|
||||
// -> y:[237,410]).
|
||||
const CLASSEUR_X1 = 37, CLASSEUR_X2 = 78;
|
||||
const CLASSEUR_Y1 = 237, CLASSEUR_Y2 = 410;
|
||||
// Chevalet supprimé le 3 septembre 2026 (voir journal) : plus d'objet à
|
||||
// cet endroit sur la nouvelle photo, Jean-Marc l'a rangé.
|
||||
// Zone cliquable/survolable du micro : silhouette complète, x inchangé,
|
||||
// y:[236,406] -> y:[249,419].
|
||||
|
||||
// -------- Classeurs (7 septembre 2026 — 9 classeurs remplacent l'unique
|
||||
// classeur "Contacts" du 2 septembre) --------
|
||||
// Nouvelle photo fournie par Jean-Marc, mêmes dimensions de référence
|
||||
// 1024×457. Toutes les coordonnées ci-dessous ont été mesurées directement
|
||||
// sur cette nouvelle photo par analyse pixel par pixel (relevé des
|
||||
// transitions de couleur colonne par colonne / ligne par ligne), pas à
|
||||
// l'œil — même exigence de précision que les mesures manuelles de
|
||||
// Jean-Marc sur les versions précédentes.
|
||||
//
|
||||
// CLASSEURS_Y1/Y2 : rectangle vertical de la tranche visible, commun aux
|
||||
// 9 classeurs (mesuré quasi identique sur chacun, 230-233 en haut et
|
||||
// 409-411 en bas — la photo montre les 9 classeurs à la même hauteur).
|
||||
const CLASSEURS_Y1 = 231, CLASSEURS_Y2 = 410;
|
||||
// Rectangle vertical utile de la partie blanche de l'étiquette (mesuré
|
||||
// constant à 255-339 sur les 9 classeurs) : c'est cette hauteur, une fois
|
||||
// convertie à l'écran, qui borne la largeur du texte tourné à -90°
|
||||
// (voir calculerPoliceEtiquette() plus bas).
|
||||
const ETIQ_LABEL_Y1 = 255, ETIQ_LABEL_Y2 = 339;
|
||||
const ETIQ_CENTRE_Y_FRAC = ((ETIQ_LABEL_Y1 + ETIQ_LABEL_Y2) / 2) / IMG_H;
|
||||
// Largeur utile de la partie blanche de l'étiquette (mesurée 22-25px selon
|
||||
// le classeur, valeur basse retenue par sécurité — cette largeur borne
|
||||
// l'ÉPAISSEUR du texte tourné, donc indirectement la taille de police
|
||||
// maximale des libellés courts, voir calculerPoliceEtiquette() plus bas).
|
||||
const ETIQ_LABEL_LARGEUR = 22;
|
||||
// Marges de sécurité (10-6%) sur les deux contraintes ci-dessus. Plus
|
||||
// resserrées que la précédente version (12%) : la taille de police n'est
|
||||
// plus estimée à partir d'un ratio de largeur de caractère deviné, mais
|
||||
// mesurée pour de vrai par le navigateur lui-même (voir
|
||||
// calculerPoliceEtiquette()) — la marge ne sert donc plus qu'à absorber
|
||||
// l'écart entre la police réellement utilisée chez Jean-Marc (Segoe UI,
|
||||
// Windows) et celle du moteur qui affiche la page au moment du calcul,
|
||||
// jamais à compenser une approximation de largeur de texte.
|
||||
const ETIQ_MARGE_HAUTEUR = 0.94;
|
||||
const ETIQ_MARGE_LARGEUR = 0.90;
|
||||
// Facteur hauteur de ligne approximatif (line-height non fixé en CSS ici,
|
||||
// donc dépendant du navigateur — 1.2 est une valeur usuelle prudente) :
|
||||
// c'est cette épaisseur de ligne, pas la taille de police elle-même, qui
|
||||
// doit tenir dans ETIQ_LABEL_LARGEUR une fois le texte tourné à -90°.
|
||||
const ETIQ_LIGNE_HAUTEUR = 1.2;
|
||||
// Seuil plancher, en dessous duquel le texte deviendrait illisible quel
|
||||
// que soit le libellé (aucun classeur actuel ne devrait l'atteindre).
|
||||
const ETIQ_POLICE_MIN_FRAC = 9 / IMG_W;
|
||||
// Plafond commun aux libellés courts : épaisseur de ligne maximale qui
|
||||
// tient dans la largeur physique de l'étiquette (2ᵉ contrainte de
|
||||
// calculerPoliceEtiquette() ci-dessous, extraite ici en constante
|
||||
// partagée) — c'est la taille qu'atteignent "Contacts", "Catalogue", "En
|
||||
// cours" ou "Factures" une fois que la contrainte de hauteur cesse d'être
|
||||
// la plus stricte des deux. Sert aussi de police forcée pour "Documents"
|
||||
// (policeForceeFrac dans CLASSEURS, voir plus bas) le 7 septembre 2026
|
||||
// (soir).
|
||||
const ETIQ_POLICE_CAP_FRAC = (ETIQ_LABEL_LARGEUR * ETIQ_MARGE_LARGEUR) / ETIQ_LIGNE_HAUTEUR / IMG_W;
|
||||
|
||||
// Tableau des 9 classeurs, dans l'ordre demandé par Jean-Marc le
|
||||
// 7 septembre 2026 (photo retravaillée : le 1er classeur a glissé vers la
|
||||
// gauche et devient "Consignes", "Contacts" passe en 2e position).
|
||||
// x1/x2 : bornes horizontales de la tranche visible (classeur entier, y
|
||||
// compris le bord en plastique coloré autour de l'étiquette) — définissent
|
||||
// la zone cliquable/survolable. labelCx : centre horizontal mesuré du
|
||||
// RECTANGLE BLANC de l'étiquette lui-même (pas du classeur entier) —
|
||||
// retouché le 7 septembre 2026 après retour de Jean-Marc ("Consignes" trop
|
||||
// à droite, "Contacts"/"Catalogue" à décaler vers la gauche) : l'étiquette
|
||||
// n'est pas toujours parfaitement centrée sur la tranche du classeur,
|
||||
// mieux vaut mesurer sa propre position que supposer qu'elle coïncide avec
|
||||
// le centre de la tranche. Re-décalé une seconde fois le 7 septembre 2026
|
||||
// (soir) pour "Consignes" seul ("presque parfait", encore un peu trop à
|
||||
// droite) : -2px supplémentaires. href : écran réel derrière le classeur,
|
||||
// ou null quand cet écran n'existe pas encore — seul un classeur avec href
|
||||
// reçoit la classe "classeur-actif" (survol + clic, voir creerClasseurs()) ;
|
||||
// les 8 autres affichent leur étiquette mais ne sont pas encore cliquables,
|
||||
// faute d'écran à ouvrir (voir le journal pour la liste des écrans
|
||||
// restant à construire). policeForceeFrac : taille de police imposée (en
|
||||
// fraction de IMG_W), au lieu de la double contrainte calculée par
|
||||
// calculerPoliceEtiquette() — voir commentaire sur "Documents" ci-dessous
|
||||
// pour le seul cas qui l'utilise pour l'instant.
|
||||
const CLASSEURS = [
|
||||
{id:'consignes', label:'Consignes', x1:44, x2:79.5, labelCx:57.2, href:null},
|
||||
{id:'contacts', label:'Contacts', x1:79.5, x2:118, labelCx:96.6, href:'liste_contacts.html'},
|
||||
{id:'catalogue', label:'Catalogue', x1:118, x2:155, labelCx:135.0, href:null},
|
||||
// "Documentations" remplacé par "Documents" le 7 septembre 2026 (soir),
|
||||
// à la demande de Jean-Marc (le libellé complet restait peu lisible même
|
||||
// après le passage à measureText() : un mot de 14 lettres doit beaucoup
|
||||
// rétrécir pour tenir dans la hauteur de l'étiquette). Police forcée
|
||||
// (policeForceeFrac, voir ETIQ_POLICE_CAP_FRAC plus haut) plutôt que
|
||||
// laissée au calcul automatique : "Documents" (9 lettres, comme
|
||||
// "Consignes") tiendrait déjà largement dans la hauteur disponible même
|
||||
// à la taille plafond commune aux libellés courts — autant l'aligner
|
||||
// explicitement sur ce plafond pour garantir la même taille bien lisible
|
||||
// que "Contacts"/"Catalogue"/"En cours"/"Factures", plutôt que de
|
||||
// dépendre d'un calcul qui pourrait légèrement varier d'un libellé à
|
||||
// l'autre. id conservé en interne à 'documentations' (cohérence avec
|
||||
// l'historique de la photo/des mesures), seul le libellé affiché change.
|
||||
{id:'documentations', label:'Documents', x1:155, x2:192.5, labelCx:173.9, href:null},
|
||||
{id:'prospection', label:'Prospection', x1:192.5, x2:234, labelCx:212.6, href:null},
|
||||
// "Communication" remplacé par "Réseaux" le 7 septembre 2026 (soir), à la
|
||||
// demande de Jean-Marc : ce classeur regroupe la génération de posts
|
||||
// Instagram et la veille concurrentielle sur les réseaux sociaux —
|
||||
// "Réseaux" colle mieux à cet usage que le générique "Communication",
|
||||
// en plus d'être bien plus court (donc bien plus lisible une fois
|
||||
// tourné à -90°, voir calculerPoliceEtiquette()). id conservé en interne
|
||||
// à 'communication' (cohérence avec l'historique de la photo/des
|
||||
// mesures), seul le libellé affiché change.
|
||||
{id:'communication', label:'Réseaux', x1:234, x2:274.5, labelCx:253.0, href:null},
|
||||
{id:'en-cours', label:'En cours', x1:274.5, x2:312.5, labelCx:293.0, href:null},
|
||||
{id:'factures', label:'Factures', x1:312.5, x2:352.5, labelCx:332.5, href:null},
|
||||
{id:'journalier', label:'Journalier', x1:352.5, x2:395.5, labelCx:372.6, href:null},
|
||||
];
|
||||
|
||||
// Zone cliquable/survolable du micro, centre du rond noir de la grille
|
||||
// (icône) et zone des post-its personnels : AUCUN changement le 7
|
||||
// septembre 2026 — vérifié par comparaison pixel à pixel avec l'ancienne
|
||||
// photo, identiques au-delà de x≈400 (voir commentaire CSS plus haut).
|
||||
const MICRO_ZONE_X1 = 465, MICRO_ZONE_X2 = 543;
|
||||
const MICRO_ZONE_Y1 = 249, MICRO_ZONE_Y2 = 419;
|
||||
// Centre du rond noir en haut du micro (grille), pour l'icône seule :
|
||||
// (512, 255) -> (512, 268).
|
||||
const ICONE_CENTRE_X = 512, ICONE_CENTRE_Y = 268;
|
||||
const ICONE_TAILLE_FRAC = 22 / IMG_W;
|
||||
// Pastille "nombre de contacts", sous le trou cerclé de métal du classeur
|
||||
// Contacts : centre (58, 381) -> (58, 394).
|
||||
const PASTILLE_CONTACTS_CENTRE_X = 58, PASTILLE_CONTACTS_CENTRE_Y = 394;
|
||||
// Pastille "nombre de contacts" — sous le trou de préhension du classeur
|
||||
// Contacts, à mi-chemin entre ce trou et le bas du classeur (retour de
|
||||
// Jean-Marc le 7 septembre 2026 : la 1ʳᵉ mesure avait par erreur calé la
|
||||
// pastille SUR le trou lui-même, pas en dessous). Trou de préhension
|
||||
// repéré par sa forme ovale sombre, mesurée sur la nouvelle photo :
|
||||
// x≈[89,107] (centre 98), y≈[360,374] (bas du trou 374). Milieu entre ce
|
||||
// bas de trou (374) et le bas du classeur (CLASSEURS_Y2=410) : (374+410)/2
|
||||
// = 392.
|
||||
const PASTILLE_CONTACTS_CENTRE_X = 98, PASTILLE_CONTACTS_CENTRE_Y = 392;
|
||||
const PASTILLE_CONTACTS_DIAM_FRAC = 17 / IMG_W;
|
||||
|
||||
// Zone des post-its personnels (2 septembre 2026, premier affichage réel
|
||||
@@ -418,9 +545,10 @@ Créé par Mevelia Solutions">
|
||||
// 30% de la largeur de l'écran depuis le bord droit (1024 × 0.30 ≈ 307).
|
||||
// Bas de la zone (Y2) recalculé le 3 septembre 2026 : n'était plus borné
|
||||
// par le chevalet (supprimé) mais par la nouvelle ligne de bureau
|
||||
// (CLASSEUR_Y2 = 410), avec la même marge de sécurité qu'avant (~15px) —
|
||||
// 352 devient 395, quelques pixels de plus qu'un simple +13 puisque
|
||||
// l'espace se libère réellement, comme relevé par Jean-Marc. Grille
|
||||
// (CLASSEURS_Y2 = 410, ex-CLASSEUR_Y2 avant le passage à 9 classeurs le
|
||||
// 7 septembre — valeur inchangée), avec la même marge de sécurité qu'avant
|
||||
// (~15px) — 352 devient 395, quelques pixels de plus qu'un simple +13
|
||||
// puisque l'espace se libère réellement, comme relevé par Jean-Marc. Grille
|
||||
// 4 colonnes × 5 lignes = 20 cases, volontairement alignée sur
|
||||
// CAPACITE_TABLEAU_PAR_UTILISATEUR (todolist.php) — à faire évoluer
|
||||
// ensemble le jour où cette capacité changera.
|
||||
@@ -446,6 +574,84 @@ Créé par Mevelia Solutions">
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
// Crée les 9 zones cliquables + étiquettes de classeurs, une seule fois
|
||||
// (voir tableau CLASSEURS plus haut) — même principe que les post-its,
|
||||
// appelée une fois au chargement, positionnée ensuite (comme tout le
|
||||
// reste) dans positionner(). Un classeur avec href (voir CLASSEURS) est
|
||||
// un vrai lien <a> ; les 8 autres, sans écran derrière pour l'instant,
|
||||
// sont de simples <div> — pas de lien mort ni de survol trompeur (voir
|
||||
// commentaire CSS .classeur-zone plus haut).
|
||||
function creerClasseurs() {
|
||||
CLASSEURS.forEach(function (c) {
|
||||
const zone = document.createElement(c.href ? 'a' : 'div');
|
||||
zone.className = 'classeur-zone' + (c.href ? ' classeur-actif' : '');
|
||||
zone.id = 'classeur-' + c.id;
|
||||
zone.dataset.classeurId = c.id;
|
||||
if (c.href) {
|
||||
zone.href = c.href;
|
||||
zone.title = 'Ouvrir le classeur ' + c.label.toLowerCase();
|
||||
}
|
||||
document.body.appendChild(zone);
|
||||
|
||||
const etiquette = document.createElement('div');
|
||||
etiquette.className = 'etiquette-classeur';
|
||||
etiquette.id = 'etiquette-' + c.id;
|
||||
etiquette.textContent = c.label;
|
||||
document.body.appendChild(etiquette);
|
||||
});
|
||||
}
|
||||
|
||||
// Mesure de la largeur RÉELLE d'un texte (7 septembre 2026, remplace une
|
||||
// première version qui estimait cette largeur à partir d'un ratio de
|
||||
// largeur de caractère deviné — trop approximatif : "Prospection" et
|
||||
// "Journalier" étaient ressortis trop petits à l'usage réel chez
|
||||
// Jean-Marc). Utilise un <canvas> hors-écran avec exactement la même
|
||||
// police que .etiquette-classeur (voir CSS plus haut) : measureText()
|
||||
// interroge directement le moteur de rendu de police du navigateur EN
|
||||
// COURS D'UTILISATION — celui de Jean-Marc (Segoe UI, Windows) une fois la
|
||||
// page redéployée chez lui — plutôt qu'une estimation calculée ici. Un
|
||||
// seul canvas et un petit cache par (libellé, taille) : positionner() est
|
||||
// rappelée à chaque redimensionnement de fenêtre, pas la peine de remesurer
|
||||
// à chaque fois un texte qui n'a pas changé.
|
||||
const canvasMesure = document.createElement('canvas');
|
||||
const ctxMesure = canvasMesure.getContext('2d');
|
||||
const largeursTexteMesurees = {};
|
||||
function mesurerLargeurTexte(libelle, taillePx) {
|
||||
const cle = libelle + '@' + taillePx;
|
||||
if (largeursTexteMesurees[cle] !== undefined) return largeursTexteMesurees[cle];
|
||||
ctxMesure.font = '600 ' + taillePx + 'px "Segoe UI","Arial",sans-serif';
|
||||
// letter-spacing:0.03em (voir .etiquette-classeur) ajouté à la main :
|
||||
// measureText() ne le prend pas en compte nativement de façon fiable
|
||||
// sur tous les navigateurs, un calcul explicite est plus sûr qu'un
|
||||
// support incertain de ctx.letterSpacing.
|
||||
const largeurBase = ctxMesure.measureText(libelle).width;
|
||||
const largeur = largeurBase + (libelle.length - 1) * 0.03 * taillePx;
|
||||
largeursTexteMesurees[cle] = largeur;
|
||||
return largeur;
|
||||
}
|
||||
|
||||
// Taille de police (en fraction de IMG_W) pour l'étiquette d'un classeur —
|
||||
// double contrainte, la plus stricte des deux l'emporte :
|
||||
// 1) La largeur RÉELLE du texte (mesurée ci-dessus à une taille de
|
||||
// référence, puis mise à l'échelle) doit tenir dans la hauteur
|
||||
// disponible de l'étiquette (ETIQ_LABEL_Y2-Y1) : c'est cette hauteur
|
||||
// qui devient la largeur du texte une fois tourné à -90°. C'est cette
|
||||
// contrainte qui borne les libellés longs ("Documentations",
|
||||
// "Communication").
|
||||
// 2) L'épaisseur d'une ligne de texte (≈ taille × ETIQ_LIGNE_HAUTEUR) doit
|
||||
// tenir dans la largeur physique de l'étiquette (ETIQ_LABEL_LARGEUR) :
|
||||
// sans cette 2ᵉ contrainte, un libellé court comme "Factures" pourrait
|
||||
// grandir au point de déborder du rectangle blanc sur les côtés.
|
||||
// Bornée par ETIQ_POLICE_MIN_FRAC en dessous (seuil de lisibilité) — pas de
|
||||
// plafond arbitraire au-dessus, les deux contraintes ci-dessus suffisent.
|
||||
function calculerPoliceEtiquette(libelle) {
|
||||
const TAILLE_REF = 100; // px CSS, taille arbitraire pour la mesure
|
||||
const largeurRef = mesurerLargeurTexte(libelle, TAILLE_REF);
|
||||
const hauteurDispoImg = (ETIQ_LABEL_Y2 - ETIQ_LABEL_Y1) * ETIQ_MARGE_HAUTEUR;
|
||||
const parContrainteHauteur = (hauteurDispoImg / largeurRef) * TAILLE_REF / IMG_W;
|
||||
return Math.max(ETIQ_POLICE_MIN_FRAC, Math.min(parContrainteHauteur, ETIQ_POLICE_CAP_FRAC));
|
||||
}
|
||||
|
||||
function positionner() {
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
const ratioImg = IMG_W / IMG_H;
|
||||
@@ -458,16 +664,22 @@ Créé par Mevelia Solutions">
|
||||
dispW = vw; dispH = dispW / ratioImg;
|
||||
offX = 0; offY = (vh - dispH) / 2;
|
||||
}
|
||||
const etiquette = document.getElementById('etiquette-contacts');
|
||||
etiquette.style.left = (offX + ETIQ_CENTRE_X_FRAC * dispW) + 'px';
|
||||
etiquette.style.top = (offY + ETIQ_CENTRE_Y_FRAC * dispH) + 'px';
|
||||
etiquette.style.fontSize = (ETIQ_POLICE_FRAC * dispW) + 'px';
|
||||
|
||||
const classeur = document.getElementById('classeur-contacts');
|
||||
classeur.style.left = (offX + (CLASSEUR_X1 / IMG_W) * dispW) + 'px';
|
||||
classeur.style.top = (offY + (CLASSEUR_Y1 / IMG_H) * dispH) + 'px';
|
||||
classeur.style.width = (((CLASSEUR_X2 - CLASSEUR_X1) / IMG_W) * dispW) + 'px';
|
||||
classeur.style.height = (((CLASSEUR_Y2 - CLASSEUR_Y1) / IMG_H) * dispH) + 'px';
|
||||
// Les 9 classeurs (zone cliquable + étiquette tournée, voir
|
||||
// creerClasseurs() et calculerPoliceEtiquette() plus haut).
|
||||
CLASSEURS.forEach(function (c) {
|
||||
const zone = document.getElementById('classeur-' + c.id);
|
||||
zone.style.left = (offX + (c.x1 / IMG_W) * dispW) + 'px';
|
||||
zone.style.top = (offY + (CLASSEURS_Y1 / IMG_H) * dispH) + 'px';
|
||||
zone.style.width = (((c.x2 - c.x1) / IMG_W) * dispW) + 'px';
|
||||
zone.style.height = (((CLASSEURS_Y2 - CLASSEURS_Y1) / IMG_H) * dispH) + 'px';
|
||||
|
||||
const etiquette = document.getElementById('etiquette-' + c.id);
|
||||
etiquette.style.left = (offX + (c.labelCx / IMG_W) * dispW) + 'px';
|
||||
etiquette.style.top = (offY + ETIQ_CENTRE_Y_FRAC * dispH) + 'px';
|
||||
const taillePoliceFrac = (c.policeForceeFrac !== undefined) ? c.policeForceeFrac : calculerPoliceEtiquette(c.label);
|
||||
etiquette.style.fontSize = (taillePoliceFrac * dispW) + 'px';
|
||||
});
|
||||
|
||||
const microDesk = document.getElementById('micro-desk');
|
||||
microDesk.style.left = (offX + (MICRO_ZONE_X1 / IMG_W) * dispW) + 'px';
|
||||
@@ -530,6 +742,7 @@ Créé par Mevelia Solutions">
|
||||
postit.style.transform = 'rotate(' + rotation.toFixed(1) + 'deg)';
|
||||
});
|
||||
}
|
||||
creerClasseurs();
|
||||
window.addEventListener('resize', positionner);
|
||||
positionner();
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 106 KiB |
@@ -59,9 +59,9 @@ Pour les trois documents de ce projet (ce journal — résumé/index —, les R
|
||||
- 29. Interprétation vocale des contacts par LLM
|
||||
- 30. Chantier des consignes — moteur générique
|
||||
|
||||
*(Numérotation et titres repris du découpage fait par Jean-Marc le 31 août 2026, légèrement reformulés par lui à cette occasion — se référer aux fichiers eux-mêmes pour le libellé exact. Section 27 (Pilotage vocal) validée de bout en bout et son fichier définitif déposé dans `/journal` le 2 septembre 2026. Section 28 (Écran de fond) ouverte le 2 septembre 2026, toujours **en cours** — passée d'une maquette graphique exploratoire à un premier essai fonctionnel réel, puis (soirée du 2 au 3 septembre) à une pastille de comptage sur le classeur Contacts et à un premier affichage réel des post-its personnels sur le tableau de liège. Section 29 (Interprétation vocale des contacts par LLM) ouverte et son fichier définitif déposé le 4 septembre 2026 après validation de bout en bout sur un premier corpus réel ; fichiers de section à déposer par Jean-Marc, voir Statut actuel. Section 30 (Chantier des consignes — moteur générique) ouverte le 6 septembre 2026, fichier définitif rédigé le soir même après validation de bout en bout du mécanisme sur des cas réels ; fichier de section à déposer par Jean-Marc, voir Statut actuel.)*
|
||||
*(Numérotation et titres repris du découpage fait par Jean-Marc le 31 août 2026, légèrement reformulés par lui à cette occasion — se référer aux fichiers eux-mêmes pour le libellé exact. Section 27 (Pilotage vocal) validée de bout en bout et son fichier définitif déposé dans `/journal` le 2 septembre 2026. Section 28 (Écran de fond) ouverte le 2 septembre 2026, toujours **en cours** — passée d'une maquette graphique exploratoire à un premier essai fonctionnel réel, puis (soirée du 2 au 3 septembre) à une pastille de comptage sur le classeur Contacts et à un premier affichage réel des post-its personnels sur le tableau de liège. Section 29 (Interprétation vocale des contacts par LLM) ouverte et son fichier définitif déposé le 4 septembre 2026 après validation de bout en bout sur un premier corpus réel ; fichiers de section à déposer par Jean-Marc, voir Statut actuel. Section 30 (Chantier des consignes — moteur générique) ouverte le 6 septembre 2026, fichier définitif rédigé le soir même après validation de bout en bout du mécanisme sur des cas réels ; **substantiellement complétée le 7 septembre 2026** (hiérarchisation cohérence/Tests/IA, traçabilité de l'agent, `REA_NomAffiche`, premier appel IA réel dans une consigne avec avertissement bloquant) — fichier de section mis à jour à déposer par Jean-Marc, voir Statut actuel.)*
|
||||
|
||||
## Statut actuel (2-6 septembre 2026)
|
||||
## Statut actuel (2-7 septembre 2026)
|
||||
|
||||
**Écrans "Identification" et "Liste des contacts"** (avec panneau détail intégré) : aboutis depuis le 25 août 2026, testés en production. **Prospection** (formulaire externe, vérification mail, envoi de doc) : livrée et testée en conditions réelles le 26 août 2026 (section 18). **Dépôt Git** en place depuis le 27-28 août (section 20), avec circuit de livraison FileZilla → SSH VPS → `git push` (section 23) — l'écriture directe par Claude reste bloquée côté proxy Anthropic, contournement adopté sans gêne. **MIRADOR** (authentification centralisée multi-bases, identifiant = email) branché et validé à deux utilisateurs le 30 août 2026, déployé sur `aspic.mvl-tls.fr` (développement) et `crm.aspic.bzh` (test), chacun avec son propre environnement Docker (sections 21, 24, 25).
|
||||
|
||||
@@ -101,6 +101,8 @@ Version incrémentée une seule fois pour l'ensemble de cette session (pastille
|
||||
|
||||
**6 septembre 2026 : chantier des consignes — un moteur générique pour sortir du code en dur (V1.14.15).** Détail complet en section 30 du journal, fil conceptuel complet en Réflexions stratégiques, section 15. Résumé : reprise le soir même, sous forme concrète, de la distinction règles/consignes esquissée le matin — première mise en œuvre volontairement circonscrite à la fiche Contacts (création/modification). Deux tables créées par Jean-Marc (`ASP_Fonctions` : catalogue de fonctions génériques ; `ASP_Consignes` : instructions actives par contexte/champ), un moteur générique unique (`moteur_consignes.php`, fonction `appliquerConsignes()`) qui lit ces tables à l'exécution et applique traitements (mise en forme silencieuse) puis tests (validation, avec message d'erreur personnalisé), et une bibliothèque de fonctions du catalogue (`consignes_fonctions.php` : `MAJUSCULE`, `NON_VIDE`, `PREMIERE_LETTRE_MAJUSCULE`). `contacts.php` réduit à un seul appel au moteur, plus aucun contrôle de champ écrit en dur. Détour initial corrigé par Jean-Marc (Claude avait d'abord codé en dur un contrôle par champ au lieu du moteur générique demandé), puis abandon délibéré de la validation en temps réel côté écran au profit de la robustesse — révélant au passage un bug préexistant (message d'erreur générique affiché au lieu du message réel du serveur), corrigé. Ajustements d'interface demandés en cours de test : toast d'erreur centré et rouge sur le panneau de saisie, rendu correct des retours à la ligne saisis dans un message d'erreur. Preuve de concept validée par Jean-Marc en fin de session : ajout de nouvelles consignes (`NON_VIDE` sur Prénom et Ville) sans la moindre modification de fichier PHP/JS, comportement immédiatement actif avec les fichiers déjà livrés. Version incrémentée une seule fois pour l'ensemble de cette session, sur les deux compteurs anticipés dès le début (code et conception) : **V1.14.15**.
|
||||
|
||||
**7 septembre 2026 : chantier des consignes — hiérarchisation, traçabilité et premier appel IA réel dans une consigne [Évolutions majeures] (V1.15.17).** Détail complet en section 30 du journal, fil stratégique en Réflexions stratégiques, section 17. Résumé : reprise le lendemain matin par une session dédiée au lien entre Claude et l'ordinateur de Jean-Marc (voir Réflexions stratégiques, section 16 — chantier finalement clos, sans suite), puis, le soir, extension substantielle du chantier des consignes ouvert le 6 septembre. Un badge de chargement générique (`consignes_apercu.php`, convention de fonction jumelle `_seraitDeclenchee`) prévient désormais l'utilisateur qu'un appel IA va réellement se produire avant qu'il ne soit lancé. Bug de coût découvert par Jean-Marc et corrigé structurellement : une soumission vouée à échouer sur un Test déclenchait quand même, en pure perte, un Traitement IA coûteux — le moteur exécute désormais trois phases strictement séquentielles (Traitements de cohérence, puis Tests, puis seulement si tout est passé les Traitements IA), sur la base d'un seul attribut déjà présent au catalogue (`FON_VisuelAttenteIA`). Traçabilité renforcée : un nom d'agent resté codé en dur ("Mat") a été remplacé par une source unique de vérité (`CNS_Agent`, la consigne elle-même) ; nouvelle colonne `REA_NomAffiche` sur `ASP_Realisations`, un instantané écrit une seule fois à la création (session utilisateur côté humain, nom d'agent côté IA) plutôt qu'une jointure recalculée à la lecture — écartée pour sa complexité et son incompatibilité avec une future architecture multi-bases (MIRADOR). Renommage `ASP_Agents.AGN_Nom` → `AGN_Prenom` à l'origine d'une régression du pilotage vocal (cinq fichiers pré-existants non mis à jour, "aucun impact" affirmé à tort par Claude sans avoir vérifié) — corrigée le soir même, leçon actée pour la suite (grep systématique avant d'affirmer l'absence d'impact d'un changement de schéma). Nouveau Test générique `consigne_sans_chiffre()`, appliqué par Jean-Marc sur Nom/Prénom/Ville sans retoucher un seul fichier. Premier vrai appel IA dans une consigne : `consigne_completer_code_postal()` journalise désormais un échec (ville ambiguë, ex. Paris) au lieu de rester silencieuse, via une nouvelle clé réservée générique `_avertissement` affichée immédiatement à l'écran — trois itérations d'affinage de ce retour visuel dans la soirée, jusqu'à un message réellement bloquant (bouton "Valider" explicite, pas de disparition automatique) qui ne fait que finaliser l'affichage sans jamais redéclencher un enregistrement. Version incrémentée deux fois au fil de cette même journée de travail (code en tout début, conception en fin de session, section 17 des Réflexions stratégiques étant jugée majeure) : **V1.15.17**.
|
||||
|
||||
**Reste à faire, noté au passage :**
|
||||
- Décider s'il faut renforcer le prompt d'extraction pour neutraliser les mots parasites/formules de politesse dictés autour d'une adresse mail (bug identifié le 4 septembre sur le corpus réel, correction non encore actée par Jean-Marc).
|
||||
- Clarifier l'origine de la coupure de fin de dictée observée sur un essai du 4 septembre (minuteur de silence à 3,5s vs interruption réelle) avant de décider si `DUREE_SILENCE_ARRET_MS` (`contact_ia_panel.js`) doit être allongée.
|
||||
@@ -125,7 +127,7 @@ Version incrémentée une seule fois pour l'ensemble de cette session (pastille
|
||||
- Revoir les règles de calcul des pastilles rouge/orange/vert (`calculerGroupe()`) pour éviter un tableau trop chargé en rouge/orange maintenant que le vert quitte le tableau immédiatement.
|
||||
- Décider du comportement du clic sur un post-it (aujourd'hui inerte) : ouvrir `liste_todolist.html` sur la tâche précise, ou afficher un détail directement sur l'écran de fond.
|
||||
- Capacité du tableau (20) et taille de la grille (4×5) dans `ecran_fond.html`/`todolist.php` sont des valeurs provisoires posées avant d'avoir les vrais pixels de la version finale — à ajuster ensemble après usage réel prolongé.
|
||||
- Concevoir un nouveau type de consigne pour les cas nécessitant un appel IA en cours de traitement (ex. retrouver le code postal d'une ville saisie sans code postal) — contrat de retour plus riche que Traitement/Test actuel (silencieux / besoin de précision / question à poser), gestion d'échec réseau, coût et latence d'un appel externe (chantier des consignes, section 30) — session dédiée à prévoir.
|
||||
- Concevoir le contrat de retour complet à trois issues (silencieux / besoin de précision / question à poser) pour les consignes à appel IA — le 7 septembre n'en a construit qu'un premier incrément à deux issues (succès silencieux / échec avec avertissement bloquant), voir section 30. À généraliser le jour où un second cas concret en montrera le besoin réel, pas par anticipation.
|
||||
- Écran de gestion des consignes par le boss (au lieu d'`INSERT`/`UPDATE` SQL à la main) — à construire après stabilisation du contrat ci-dessus, pour ne pas le refaire (chantier des consignes, section 30).
|
||||
- Réflexion sur un catalogue restreint de consignes/fonctions avancées à proposer aux futurs utilisateurs de l'agence, sans leur ouvrir la création libre de A à Z — argument commercial identifié par Jean-Marc le 6 septembre (voir réflexions stratégiques, section 15).
|
||||
- Accumulation de plusieurs messages d'erreur à la fois dans le moteur de consignes (aujourd'hui, seul le premier test en échec est remonté).
|
||||
|
||||
@@ -230,3 +230,53 @@ Point explicitement acté avec Jean-Marc : **aucune solution, gratuite ou payant
|
||||
3. Réfléchir à un **catalogue restreint et curaté** de consignes et de fonctions avancées, à proposer aux futurs clients de l'agence — Jean-Marc a été explicite sur le fait qu'il n'envisage pas d'ouvrir à ses futurs utilisateurs une création libre de consignes avancées "de A à Z" ; il y voit au contraire un argument commercial pour de futures mises à jour payantes ("Possibilité de nouvelles consignes pour vos assistants"). Ce point rejoint directement la distinction actée plus haut entre mécanisme générique (partagé, neutre) et catalogue de fonctions (à curater soigneusement, puisqu'il devient potentiellement un produit en soi).
|
||||
|
||||
**Statut : premier cas concret bâti et validé, philosophie plus large confirmée par la pratique.** Le moteur générique de consignes existe, fonctionne, et a déjà changé de comportement une fois sans code — scellant, à l'échelle d'un cas modeste, l'intuition posée le matin même en section 14. Trois directions ouvertes pour la suite (consignes IA, écran de gestion, catalogue commercial), aucune engagée à ce stade au-delà de la discussion.
|
||||
|
||||
## 16. Lier une session à l'ordinateur de Jean-Marc — vers la fin des allers-retours de fichiers, et une question de fond sur la visibilité du code (7 septembre 2026)
|
||||
|
||||
**Origine — une question posée en marge, pas un chantier planifié.** Jean-Marc a repéré, dans des mails d'Anthropic, l'existence d'une fonctionnalité permettant de donner à Claude un accès en lecture/écriture direct à un ou plusieurs répertoires de son poste, et a demandé confirmation. Vérification faite (documentation officielle Anthropic) : la fonctionnalité existe bien — une session peut être liée à un ordinateur via l'application desktop Claude ("Link to this computer"), avec ensuite un octroi d'accès dossier par dossier (jamais le disque entier d'un bloc), strictement à la demande de l'utilisateur.
|
||||
|
||||
**Le vrai motif, une fois creusé : soulager une friction bien réelle du circuit de livraison actuel.** Le mode de fonctionnement en place depuis le début du projet (fichiers envoyés en pièce jointe de conversation, à télécharger, parfois renommés en double par le navigateur, puis copiés/renommés à la main dans le dossier de transfert SFTP de Jean-Marc) fonctionne mais coûte de l'attention à chaque fichier, avec un risque concret d'erreur relevé par Jean-Marc lui-même : *"Parfois, notamment quand nos échanges sont intenses, je loupe un téléchargement et je suis aux fraises."* Deux irritants précis identifiés et confirmés techniquement réglés par ce mécanisme :
|
||||
- Le contournement `.js.txt` pour les fichiers JavaScript (nécessaire jusqu'ici uniquement parce que la livraison passait par le téléchargement navigateur, peu compatible avec ce type de fichier) — disparaît entièrement avec une écriture directe sur disque.
|
||||
- Le renommage automatique en doublon (`toto_47.php` au lieu d'écraser `toto.php`) — même cause, même remède : en écrivant directement dans le dossier cible, le fichier livré porte son nom final et écrase silencieusement l'ancien, sans étape de tri manuel.
|
||||
|
||||
**Mécanique vérifiée dans la documentation officielle, à retenir pour la mise en œuvre :**
|
||||
- Une fois un dossier accordé pour une tâche/projet lié à un ordinateur, l'autorisation est retenue d'une session à l'autre — pas besoin de la redonner à chaque ouverture. Condition permanente cependant : l'application desktop Claude doit être ouverte et la machine joignable au moment où une session cloud veut effectivement toucher aux fichiers ; application fermée, le pont est simplement indisponible (pas une erreur, pas une perte de la permission).
|
||||
- Concernant le NAS Synology de Jean-Marc, accédé sous Windows via une adresse IP locale : un partage réseau **mappé sur une lettre de lecteur** (ex. `Z:\`) est pris en charge par le sélecteur de dossier ; un chemin UNC brut (`\\192.168.x.x\partage`) ne l'est pas directement. Jean-Marc a confirmé que son NAS est déjà monté par lettre — pas de point de blocage de ce côté. Seule variabilité relevée par Jean-Marc lui-même : le NAS peut se mettre en veille en cas d'inactivité prolongée et mettre un instant à répondre, mais reste actif par construction lorsqu'il est à son bureau.
|
||||
|
||||
**Décision : Jean-Marc prépare un répertoire réseau dédié au développement d'ASPIC (`Développement`), avec une arborescence calquée sur celle du VPS**, pour que déposer un fichier reçu de Claude s'y réduise à un simple report dans le bon sous-dossier, sans réflexion supplémentaire. Ce pont ne remplace à aucun moment la gestion du dépôt GitHub, que Jean-Marc continue d'opérer intégralement lui-même — **précision explicite de sa part : la clé GitHub ne sera pas donnée à Claude dans le cadre de cette organisation**, ce qui clôt d'un coup la question du blocage de push déjà bien documentée (journal de développement, section 23) : le sujet devient sans objet plutôt que contourné.
|
||||
|
||||
**Question de fond soulevée en cours de discussion : la documentation soignée du code a un revers.** Jean-Marc a exprimé une satisfaction ancienne et jamais obtenue de ses développeurs précédents — voir le code commenté avec soin, but qu'il avait longtemps réclamé sans l'obtenir — tout en identifiant lui-même la contrepartie : plus un fichier est documenté, plus il informe un œil indiscret qui y aurait accès (un visiteur via "Afficher le code source" sur les fichiers HTML/JS réellement exposés au navigateur ; ou, dans une hypothèse plus lointaine, un client final ayant lui-même accès aux fichiers de son propre hébergement, si le modèle (a) de la section 5 — éditeur déployé chez le client — était un jour retenu pour un projet one-shot). Rappel de contexte déjà posé et confirmé toujours valable : les fichiers PHP ne sont eux-mêmes jamais transmis au navigateur (exécution côté serveur), donc leurs commentaires ne sont, par construction, jamais visibles d'un simple visiteur — le risque concret porte avant tout sur les fichiers HTML/JS, et plus largement sur toute situation où quelqu'un obtiendrait un accès direct aux fichiers sources eux-mêmes plutôt qu'au seul rendu navigateur.
|
||||
|
||||
**Solution retenue : une version "exploitation" du code, générée à la demande, jamais systématiquement.** Plutôt que de renoncer à la qualité de documentation acquise sur ce projet (dont la valeur pour la continuité du travail avec Claude reste entière, voir section 2), Jean-Marc a proposé de pouvoir demander, ponctuellement et une fois l'application arrivée à un certain point de maturité, une copie des sources débarrassée de tout commentaire — à déployer à la place de la version pleinement documentée, celle-ci restant la référence de travail. Décision actée : construire cet utilitaire (retrait de commentaires par analyse syntaxique réelle du langage — pas une simple recherche/remplacement de texte, qui risquerait de corrompre une chaîne de caractères contenant `//` ou un délimiteur de commentaire), à ranger dans un sous-dossier `Exploitation` du futur répertoire réseau, en miroir de `Développement`. Portée explicitement limitée, pour ne pas se tromper sur ce que ça protège réellement : un simple retrait de commentaires (le code, sa mise en forme et ses noms de variables restent identiques, ce n'est pas une minification ni un chantier d'obscurcissement) réduit la fuite d'information gratuite vers un œil curieux, mais ne constitue pas une vraie protection contre un acteur déterminé — cohérent avec le constat déjà posé en section 5 (aucune protection réelle n'existe pour du PHP/JS vanilla livré tel quel).
|
||||
|
||||
**Confrontation au réel : la demande d'accès a buté sur un bug documenté de Windows, pas sur une erreur de configuration.** Une fois le répertoire préparé côté NAS, la demande d'octroi pour `G:\01. Version 1\000. Développement` a été refusée par le mécanisme de sélection de dossier, avec un message ne pointant vers aucune cause côté Jean-Marc ("Ask the user to connect the folder they want using the folder picker on their device, or pick a different folder"). Plutôt que de proposer une manipulation au hasard, recherche faite : il s'agit d'un bug Windows réel et déjà documenté (plusieurs tickets ouverts sur le dépôt public `anthropics/claude-code`, dont #38468, #24927, #24964) — le sélecteur de dossier de l'application desktop rejette actuellement, sous Windows, tout dossier situé hors du profil utilisateur (`C:\Users\<utilisateur>\`), y compris un lecteur réseau mappé sur une lettre pourtant explicitement pris en charge par la documentation officielle ; les jonctions/liens symboliques ne contournent pas le problème, la résolution du chemin réel bloquant également ce chemin de traverse. Statut du bug à la date de cette session : non résolu, ticket fermé comme doublon sans correctif. Une tentative parallèle, créer un nouveau projet directement à partir d'un dossier pour voir si un autre point d'entrée existait, n'a fait apparaître aucune option de partage de fichiers. Conclusion honnête tirée de cet épisode, formulée par Jean-Marc lui-même et retenue comme point de vigilance pour la suite des échanges : une fonctionnalité présentée comme possible en théorie peut ne pas l'être réellement au moment de passer à l'action — une raison de plus, au-delà de ce chantier précis, de rester prudent sur ce qu'affirme Claude tant que ce n'est pas vérifié en conditions réelles.
|
||||
|
||||
**Une seule alternative restait disponible — un dossier sous `C:\` — et Jean-Marc l'a refusée, pour deux raisons distinctes et toutes deux jugées valables.** La première, posée sous forme d'image par Jean-Marc lui-même : donner l'accès à une pièce, même partiel, se choisit d'abord sur la pièce la moins sensible — entre un coffre-fort à espace réduit et une pièce d'armoires de rangement, l'accès s'ouvre sur la seconde, jamais sur le premier ; le disque `C:` héberge le système et le profil de l'utilisateur, il n'a pas vocation à devenir un point d'échange de fichiers de développement, quel que soit le soin apporté à la restriction du dossier précis accordé. La seconde raison, plus concrète et finalement décisive : une expérience vécue par Jean-Marc, un disque `C:` d'un poste stratégique arrivé à saturation à un moment où personne n'avait le temps de s'en occuper, avec pertes de données à la clé. Le volume actuel d'ASPIC sur le réseau (un peu plus de 65 Mo) est dérisoire au regard des quelque 300 Go encore libres sur le NVMe de son poste — mais Jean-Marc raisonne sur la durée du projet et sur la tendance (de nouvelles applications viendront occuper `C:` au fil du temps), pas sur l'instantané d'aujourd'hui. Claude a reconnu ce raisonnement comme fondé plutôt que d'insister sur la marge disponible dans l'immédiat.
|
||||
|
||||
**Décision finale, actée par Jean-Marc : abandon complet de l'accès direct au disque, retour à l'organisation existante.** Dans ses propres mots : *"restons zen, oublions cette idée d'accéder directement à une partie de mon disque autre que C: et restons sur l'organisation actuelle."* Le circuit de livraison redevient donc, sans changement, celui décrit en ouverture de cette section (téléchargement depuis la conversation, contournement `.js.txt` pour le JavaScript, renommage manuel `toto_nn.php` → `toto.php`) — les deux irritants identifiés en début de section restent entiers, faute d'un mécanisme technique aujourd'hui utilisable pour les résoudre. Effet positif retenu malgré tout par Jean-Marc : l'occasion de cette exploration l'a poussé à réorganiser son stockage réseau, une remise en ordre qu'il envisageait depuis longtemps et qu'il n'aurait pas faite sans ce prétexte.
|
||||
|
||||
**Fil ouvert et volontairement laissé de côté : un connecteur GitHub repéré au compte de Jean-Marc.** En cherchant le sélecteur de dossier, Jean-Marc a découvert qu'une intégration GitHub était déjà activée au niveau de son compte Anthropic, sans qu'elle soit pour autant utilisable dans la conversation en cours (les outils disponibles n'en portaient aucune trace au moment de la vérification). Jean-Marc a choisi de ne pas creuser ce point maintenant ("je vais voir plus tard pour cette connexion GitHub"), sans lien avec la décision de ne pas donner sa clé GitHub à Claude pour l'organisation de fichiers locale (section précédente), qui reste, elle, définitivement actée et non remise en cause par cette découverte.
|
||||
|
||||
**L'utilitaire de retrait de commentaires reste construit et disponible, indépendamment de l'issue de ce chantier.** Le renoncement à l'accès disque direct ne remet rien en cause côté "version exploitation" : les deux scripts (un pour le PHP par analyse syntaxique réelle via `token_get_all()`, un pour le HTML/JS via `terser` en configuration retrait-de-commentaires seul, sans compression ni renommage) ont été construits et validés ce même jour sur des fichiers réels du projet (syntaxe vérifiée après traitement, contenu fonctionnel inchangé) — prêts à resservir le jour où Jean-Marc en fera la demande, quel que soit le circuit de livraison alors en place pour les faire parvenir.
|
||||
|
||||
**Statut en fin d'échange : chantier clos, pas suspendu.** Contrairement à la formulation précédente de ce paragraphe (qui envisageait deux chantiers parallèles en cours), l'accès direct au disque de Jean-Marc est abandonné, pas différé — à ne pas rouvrir sans qu'il en fasse à nouveau la demande, par exemple si Anthropic corrige un jour le bug identifié ci-dessus. Le circuit de livraison par téléchargement reste donc la référence pour la suite du projet. Le numéro de version (partie `<conception>`) n'a pas été avancé à l'occasion de cette section, conformément à la décision initiale de Jean-Marc — la nouvelle organisation qui devait servir de test grandeur réelle pour ce déclenchement n'ayant finalement pas vu le jour, ce report reste sans objet pour l'instant plutôt que remis à plus tard.
|
||||
|
||||
## 17. Sortir du code en dur : règles et consignes comme mécanisme générique [Évolutions majeures] (7 septembre 2026)
|
||||
|
||||
**Origine.** Dès le lendemain de la première mise en œuvre du moteur générique (section 15), Jean-Marc a engagé exactement le chantier que cette section-là avait explicitement mis de côté comme méritant "une session dédiée" plutôt qu'un simple ajout au catalogue : une consigne capable d'appeler l'IA en cours de traitement (retrouver le code postal d'une ville saisie sans code postal), avec tout ce que ça implique de nouveau pour le moteur — coût réseau à ne jamais gaspiller, traçabilité de qui a réellement agi, et honnêteté du retour utilisateur en cas d'échec. Suffisamment substantiel pour justifier une nouvelle section plutôt qu'un addendum à la section 15.
|
||||
|
||||
**Le prix de la vitesse : hiérarchiser pour ne jamais payer un appel IA en pure perte.** Premier essai réel de Jean-Marc sur le nouveau moteur : une soumission vouée à échouer sur un Test (ex. champ obligatoire vide) déclenchait quand même, avant l'échec, un Traitement coûteux faisant appel à l'IA — appel réseau gaspillé, et aucune trace n'en subsistait puisque l'enregistrement entier était de toute façon rejeté ensuite. Diagnostic de Jean-Marc, structurel plutôt qu'un simple correctif ponctuel : *"il faut qu'on hiérarchise les traitements : d'abord les traitements de cohérence, après les traitements par IA."* Plutôt que de traiter ce cas précis à part, l'ordre d'exécution du moteur lui-même a été restructuré en trois phases séquentielles, distinguées par un seul attribut déjà présent dans le catalogue (`FON_VisuelAttenteIA`, voir plus bas) : d'abord les Traitements sans appel IA, puis tous les Tests (le premier échec arrête tout avant d'atteindre la moindre étape IA), et seulement une fois tous les Tests passés, les Traitements IA. Une seule distinction générique déjà présente sert donc à la fois de commande d'affichage (le badge, ci-dessous) et de garde-fou de coût — aucune nouvelle colonne, aucun cas particulier par fonction.
|
||||
|
||||
**Un badge générique pour ne jamais laisser l'utilisateur dans le flou pendant un appel réseau.** Un Traitement pouvant désormais réellement contacter un service externe avant la fin d'un enregistrement, Jean-Marc a voulu un retour visuel plutôt qu'un silence pendant l'attente. Nouvel endpoint `consignes_apercu.php`, et une convention de fonction jumelle : tout Traitement marqué `FON_VisuelAttenteIA='O'` doit être accompagné d'une fonction `<fonction>_seraitDeclenchee(array $valeurs): bool` — pure, sans appel réseau — répondant à "ce traitement se déclencherait-il réellement sur ces valeurs", utilisée pour décider, avant même de tenter l'enregistrement, s'il faut afficher un badge de chargement générique. Volontairement générique plutôt que personnalisé par agent/couleur : Jean-Marc a explicitement écarté un badge coloré par agent (*"si un jour un traitement fait appel à plusieurs agents à la suite, il ne faudrait pas que l'écran se transforme en sapin de Noël"*) et son affichage systématique à chaque enregistrement, IA ou non (*"l'utilisateur pourrait s'affoler sur le coût systématique d'une insertion client"*). Plusieurs allers-retours sur la taille du badge (130 → 50 → 25 → retour à 65px) avant validation — un détail purement cosmétique, mais qui a nécessité de regarder le rendu réel (Playwright) plutôt que de deviner une valeur en pixels dans l'abstrait.
|
||||
|
||||
**Traçabilité : qui a réellement agi, jamais un nom inventé.** Premier jet de Claude sur la journalisation d'une action IA : un prénom d'agent codé en dur ("Mat"). Repéré et corrigé après une question directe de Jean-Marc (*"d'où sors-tu le diminutif Mat ?"*) — Claude a reconnu l'avoir inventé, sans source. Décision retenue : le nom de l'agent journalisé dans une réalisation vient désormais uniquement de `CNS_Agent` (la consigne elle-même), jamais d'une valeur proposée par la fonction de traitement — même principe que celui déjà en place pour un agent en échec de Test. Dans le prolongement direct, Jean-Marc a voulu qu'un enregistrement de réalisation garde le prénom affiché de qui a agi (humain ou IA) sans avoir besoin de le recalculer plus tard : *"si on stocke le prénom de l'utilisateur d'un côté et le prénom de l'agent en dur on est cool."* Une première proposition de Claude (jointure `ASP_Users` recalculée à la lecture) a été explicitement écartée par Jean-Marc, sur deux arguments distincts : la complexité de la requête (*"Ta requête SQL risque d'être compliquée non ?"*) et surtout la portabilité multi-bases du projet (*"comment vas-tu faire pour les agents qui sont dans une autre base ?"*, en écho direct à l'architecture MIRADOR, section 8). Solution retenue : un instantané écrit une seule fois à la création de la réalisation (`REA_NomAffiche`) — côté humain via `$_SESSION['USR_Prenom']` fixé une fois pour toutes à la connexion, côté IA via le nom déjà connu de l'agent — zéro requête supplémentaire à la lecture, zéro risque de base croisée.
|
||||
|
||||
**Un renommage anodin en apparence, une leçon retenue.** À l'occasion de cette traçabilité, Jean-Marc a rappelé l'existence d'une table `ASP_Agents` déjà en place (utilisée pour le pilotage vocal, sections 11-12) et proposé d'harmoniser sa colonne `AGN_Nom` en `AGN_Prenom`, cohérent avec `USR_Prenom`. Claude a validé le renommage en affirmant "aucun impact côté code" — sans avoir grep le nom de colonne dans l'ensemble des fichiers existants. Conséquence concrète et immédiate pour Jean-Marc : *"j'ai un message qui me dit que la liste des agents n'est pas disponible !"* — cinq fichiers pré-existants (`agents.php`, `micro.js`, `contact_ia_panel.js`, `contact_ia_interprete.php`, `agent_notes.php`) référençaient encore `AGN_Nom`, cassant au passage le pilotage vocal. Réaction de Jean-Marc, sans dramatisation : *"Classique à ce stade du développement."* Leçon explicitement actée par Claude comme réflexe permanent pour la suite du projet, au-delà de ce seul incident : **grep systématiquement le nom d'une colonne dans tout le code avant d'affirmer "aucun impact" sur un changement de schéma, même quand ça semble anodin** — un changement de schéma n'est jamais gratuit à évaluer sans preuve, quelle que soit l'ancienneté ou la banalité apparente de la colonne touchée.
|
||||
|
||||
**Le premier vrai "contrat de retour enrichi" pour une consigne à appel IA — plus modeste que prévu, mais réel.** La section 15 anticipait un contrat à trois issues (silencieux / besoin de précision / question à poser) pour ce type de consigne, jugé trop tôt à concevoir dans l'abstrait. Ce qui a réellement été construit ce soir est un premier incrément volontairement plus modeste : réutilisation du mécanisme de clé réservée déjà existant (`_realisation`, jamais écrite dans `$data`, extraite par le moteur) et ajout d'une seconde clé du même type, `_avertissement` — une simple chaîne, affichée immédiatement à l'écran, sans que le front-end sache jamais d'où elle vient (mécanisme générique, pas un cas particulier "code postal" câblé côté écran). `consigne_completer_code_postal`, quand elle ne peut rien déduire d'exploitable pour une ville ambiguë ou inconnue (le cas Paris, découpée en arrondissements, sans code postal unique), laisse désormais une trace honnête dans tous les cas : une `_realisation` est journalisée que l'appel ait réussi ou échoué (Jean-Marc avait explicitement signalé l'anomalie inverse : *"il y a un appel fait à l'IA et pas de trace dans les réalisations, ce qui n'est pas normal"*), et `_avertissement` fait en plus remonter l'échec à l'utilisateur sans délai. À noter honnêtement : ce n'est pas encore le contrat à trois issues complet envisagé en section 15, seulement un succès silencieux ou un échec avec avertissement — un choix délibéré de construire la plus petite chose réelle plutôt que l'abstraction complète par anticipation, dans la continuité de la discipline déjà appliquée sur ce projet ("généraliser à partir de 3 répétitions concrètes constatées", section 6).
|
||||
|
||||
**D'un toast qui disparaît à un message qui exige une décision.** Trois allers-retours d'affinage dans la soirée sur le seul retour visuel de cet avertissement, chacun corrigeant un défaut concret repéré par l'usage réel : rouge et centré comme un vrai message d'erreur plutôt qu'un toast neutre en bas d'écran ; le panneau reste en mode édition au lieu de repasser silencieusement en mode vue (*"tu peux l'afficher en rouge et centré et rester dessus comme un message d'erreur"*) ; puis, une fois testé, le message disparaissait encore trop vite et la fiche restait visuellement "non validée" aux yeux de Jean-Marc malgré un enregistrement réellement réussi en base — solution retenue : un message réellement bloquant, sans disparition automatique, avec un unique bouton "Valider" dont le clic est la seule chose qui referme le message et fait alors passer l'affichage en mode vue. Dans les mots de Jean-Marc : *"je souhaite que le message s'affiche et reste en place [...] avec une instruction claire 'Saisissez le code postal vous-même' et que lorsque l'utilisateur clique, la fiche soit validée."* Point de conception à retenir : ce clic ne redéclenche jamais un enregistrement, il ne fait que clore l'état "attention requise" — le contact concerné est déjà bel et bien enregistré depuis le tout début de la séquence.
|
||||
|
||||
**Statut.** L'ensemble validé par Jean-Marc au fil des essais réels de la soirée même, aucune modification de code en attente au-delà de ce qui est déjà livré. Deux points laissés ouverts consciemment plutôt qu'oubliés : le contrat à trois issues complet (silencieux/besoin de précision/question à poser) reste à concevoir le jour où un second cas concret en montrera le besoin réel, plutôt que d'être généralisé dès maintenant sur la seule base de ce premier exemple ; et le clic sur "Valider" ne relance aujourd'hui aucune revalidation automatique du champ concerné — à ajouter délibérément si un futur cas le justifie, pas par anticipation.
|
||||
|
||||
Version : `<code>` avait déjà avancé plus tôt dans cette même session de travail (`V1.14.15` → `V1.15.15`) pour du code livré en amont de cette réflexion ; c'est cette section-ci qui justifie de faire avancer `<conception>` de 15 à 17 (16 ayant déjà été utilisé plus tôt le même jour par la section précédente, in fine sans incidence sur la conception d'ASPIC) — nouvelle version : **V1.15.17**.
|
||||
|
||||
@@ -67,10 +67,36 @@ Conventions retenues pour les valeurs de colonnes (choisies par Jean-Marc, diff
|
||||
|
||||
**Test réel réussi en fin de session, sans aucune modification de code :** Jean-Marc a ajouté trois nouvelles lignes dans `ASP_Consignes` (`NON_VIDE` sur `CNT_Prenom` et `CNT_AdrVille`, réutilisant la fonction déjà cataloguée) — comportement immédiatement actif après dépôt des fichiers déjà livrés, aucun fichier PHP/JS retouché. Preuve de concept validée par Jean-Marc : *"On peut dire qu'on n'est pas mauvais !"*
|
||||
|
||||
**Reste à faire, noté en fin de session :**
|
||||
- Concevoir un nouveau type de consigne pour les cas nécessitant un appel IA en cours de traitement (ex. retrouver le code postal d'une ville saisie sans code postal) — contrat de retour plus riche que Traitement/Test actuel (silencieux / besoin de précision / question à poser), gestion d'échec réseau, coût et latence d'un appel externe. Session dédiée à prévoir, pas un ajout mineur au catalogue existant.
|
||||
**Reste à faire, noté en fin de session du 6 septembre :**
|
||||
- Concevoir un nouveau type de consigne pour les cas nécessitant un appel IA en cours de traitement (ex. retrouver le code postal d'une ville saisie sans code postal) — contrat de retour plus riche que Traitement/Test actuel (silencieux / besoin de précision / question à poser), gestion d'échec réseau, coût et latence d'un appel externe. Session dédiée à prévoir, pas un ajout mineur au catalogue existant. *(Premier incrément construit dès le lendemain, voir 7 septembre 2026 ci-dessous — pas encore le contrat complet à trois issues.)*
|
||||
- Écran de gestion des consignes par le boss (au lieu de `INSERT`/`UPDATE` SQL à la main) — à construire après stabilisation du contrat ci-dessus, pour ne pas le refaire.
|
||||
- Réflexion sur un catalogue restreint de consignes/fonctions avancées à proposer aux futurs utilisateurs de l'agence, sans leur ouvrir la création libre de A à Z — argument commercial identifié par Jean-Marc ("Possibilité de nouvelles consignes pour vos assistants").
|
||||
- Accumulation de plusieurs messages d'erreur à la fois (le moteur ne remonte aujourd'hui que le premier test en échec).
|
||||
- Gestion des consignes transverses à toute la fiche (`CNS_Champ` NULL, ex. contrôle de doublon) — non traitée par le moteur actuel.
|
||||
- Distinction Saisie/Validation réellement exploitée un jour (temps réel), si le besoin redevient assez fort pour justifier la complexité écartée ce soir.
|
||||
|
||||
## Suite — 7 septembre 2026 : hiérarchisation, traçabilité, et premier appel IA réel dans une consigne
|
||||
|
||||
**Contexte stratégique complet : Réflexions stratégiques, section 17 ([Évolutions majeures]).** Ce qui suit est le compte rendu technique correspondant.
|
||||
|
||||
**1. Badge de chargement IA générique.** Nouvel endpoint `consignes_apercu.php` et convention de fonction jumelle : tout Traitement marqué `FON_VisuelAttenteIA='O'` dans `ASP_Fonctions` doit être accompagné d'une fonction `<FON_NomPHP>_seraitDeclenchee(array $valeurs): bool`, sans aucun appel réseau, qui répond seule à "ce traitement se déclencherait-il réellement sur ces valeurs". `auMoinsUneConsigneIaSeraitDeclenchee()` (`moteur_consignes.php`) interroge ces fonctions jumelles pour toutes les consignes actives du contexte/portée, avant même une tentative d'enregistrement — `liste_contacts.html` n'affiche le badge `#badge-ia-actif` que si la réponse est positive, jamais de façon systématique. Rendu : rond, centré à l'écran comme un toast d'erreur, 65px de côté après plusieurs essais (130 → 50 → 25 → 65), vérifiés au rendu réel via Playwright plutôt qu'en pixels devinés. `pointer-events:none` (jamais un obstacle au clic, même principe que les autres éléments purement informatifs de l'écran de fond).
|
||||
|
||||
**2. Hiérarchisation des trois phases d'`appliquerConsignes()`.** Bug réel découvert par Jean-Marc : une soumission vouée à échouer sur un Test déclenchait quand même, avant l'échec, un Traitement IA coûteux — appel réseau gaspillé, sans même laisser de trace puisque l'enregistrement entier était rejeté ensuite. Le moteur exécute désormais trois phases strictement séquentielles, distinguées par le seul attribut `FON_VisuelAttenteIA` déjà présent : (1) tous les Traitements de cohérence (`FON_VisuelAttenteIA != 'O'`), (2) tous les Tests (le premier échec arrête tout, avant d'atteindre la moindre étape IA), (3) seulement si tous les Tests ont réussi, les Traitements IA (`FON_VisuelAttenteIA = 'O'`). Nouvelle fonction interne `executerTraitements(array $consignesTraitement, array &$data, array &$journalisations, array &$avertissements): void`, appelée deux fois (phase 1 et phase 3) avec un sous-ensemble différent de consignes.
|
||||
|
||||
**3. Traçabilité de l'agent réel dans une réalisation journalisée.** `executerTraitements()` ne fait plus confiance à un nom d'agent proposé par la fonction de traitement elle-même (source d'un bug déjà corrigé : un prénom "Mat" resté codé en dur sans aucune source) — le nom journalisé dans `_realisation.agent` vient désormais systématiquement de `$c['CNS_Agent']` (la consigne active elle-même), tronqué à 15 caractères UTF-8, avec un repli sur la valeur de la fonction puis sur `'IA'` seulement si `CNS_Agent` est vide. Même principe déjà en place côté `$agentEchec` pour un Test en échec.
|
||||
|
||||
**4. `REA_NomAffiche` — instantané du nom affiché, jamais recalculé à la lecture.** Nouvelle colonne sur `ASP_Realisations`, remplie une seule fois à la création de chaque ligne : côté humain, `$_SESSION['USR_Prenom']` (désormais chargé et stocké en session dès `aspic.php`, à la connexion — requête étendue `SELECT USR_Nom, USR_Prenom FROM ASP_Users`) ; côté IA, le même nom d'agent que celui journalisé dans `_realisation.agent` (voir point 3). Une première approche par jointure `ASP_Users` recalculée à la lecture a été écartée (complexité de la requête, et surtout incompatibilité avec un futur agent hébergé dans une autre base MIRADOR) au profit de cet instantané écrit une seule fois — zéro requête supplémentaire à la lecture, zéro risque de base croisée. `realisations.php` (GET et POST) et `contacts.php` (`journaliserRealisationsIA()`) mis à jour en conséquence ; `liste_contacts.html` affiche `r.REA_NomAffiche || r.REA_CodeUser || ""` et la colonne "Qui ?" a été élargie de 40 à 90px pour ne plus tronquer "Jean-Marc"/"Matilda".
|
||||
|
||||
**5. Renommage `ASP_Agents.AGN_Nom` → `AGN_Prenom` (à l'initiative de Jean-Marc), et une régression corrigée.** Fait pour homogénéiser avec `USR_Prenom`. Claude a validé ce renommage en affirmant "aucun impact côté code" sans avoir grep le nom de colonne dans l'ensemble du projet — cinq fichiers pré-existants du pilotage vocal (`agents.php`, `micro.js`, `contact_ia_panel.js`, `contact_ia_interprete.php`, `agent_notes.php`) référençaient encore `AGN_Nom`, cassant la liste des agents ("la liste des agents n'est pas disponible !"). Tous corrigés le même soir. Leçon actée pour la suite (voir Réflexions stratégiques, section 17) : grep systématiquement le nom d'une colonne dans tout le code avant d'affirmer "aucun impact" sur un changement de schéma.
|
||||
|
||||
**6. `consigne_sans_chiffre()` — nouveau Test générique.** `consignes_fonctions.php` : `return trim($valeur) === '' || !preg_match('/\d/', $valeur);`. Ajoutée au catalogue (`ASP_Fonctions`, code `SANS_CHIFFRE`) puis appliquée par Jean-Marc lui-même via trois nouvelles lignes `ASP_Consignes` (Nom, Prénom, Ville) — aucun fichier PHP/JS retouché pour l'étendre à un troisième champ, confirmant une fois de plus le principe du chantier.
|
||||
|
||||
**7. `consigne_completer_code_postal()` — premier vrai appel IA dans une consigne, avec trace honnête en cas d'échec.** Reprend le contrat existant (`_realisation`, clé réservée déjà en place) et introduit une nouvelle clé réservée symétrique, `_avertissement` — une simple chaîne, affichée immédiatement à l'écran par le front-end sans qu'il sache jamais d'où elle vient. Quand l'IA ne peut rien déduire d'exploitable (ville ambiguë comme Paris, découpée en arrondissements) : une `_realisation` est journalisée dans tous les cas (même en échec — corrige l'anomalie signalée par Jean-Marc : un appel IA sans la moindre trace), et `_avertissement` est en plus renvoyé au front. `moteur_consignes.php` (`executerTraitements()`) extrait les deux clés en parallèle dans un nouveau tableau `$avertissements`, remonté par `appliquerConsignes()` (nouveau paramètre `?array &$avertissements = null`) puis par `contacts.php` dans la réponse JSON (`'avertissements' => $avertissementsIA`).
|
||||
|
||||
**8. Affichage de l'avertissement côté écran — trois itérations dans la soirée.** D'abord un toast neutre, identique aux autres. Puis rouge et centré comme un vrai message d'erreur (`afficherToast(message, true)`), et le panneau reste en mode édition au lieu de repasser en mode vue après l'enregistrement (`afficherDetailPourId(idContact, avertissements.length ? "edition" : "vue")`) — pour que l'utilisateur puisse corriger tout de suite le champ resté incomplet, `contactCourant` étant malgré tout rafraîchi pour éviter un double POST en cas de nouvelle tentative d'enregistrement. Puis, une fois testé, le message disparaissait encore trop vite et la fiche restait visuellement "non validée" alors qu'elle était bien enregistrée : `afficherToast()` accepte désormais un 4ᵉ paramètre optionnel (`onValider`) qui, s'il est fourni, supprime la disparition automatique et ajoute un bouton "Valider" (nouvelle classe `.toast-bouton`) — le clic ferme le message et exécute `onValider`, ici un simple `basculerModeDetail("vue")` (aucune revalidation, aucun nouvel enregistrement déclenché, le contact étant déjà enregistré depuis le début de la séquence). Le texte de l'avertissement, côté `consignes_fonctions.php`, a été rendu plus impératif ("Saisissez le code postal vous-même.") en cohérence avec ce fonctionnement bloquant.
|
||||
|
||||
**Version.** `<code>` déjà avancé en tout début de cette session de travail (`V1.14.15` → `V1.15.15`, avant même le badge IA du point 1). `<conception>` avancé en fin de session à `17` (Réflexions stratégiques, nouvelle section 17) — version finale de la session : **V1.15.17**.
|
||||
|
||||
**Reste à faire, noté en fin de session du 7 septembre :**
|
||||
- Le contrat de retour à trois issues (silencieux / besoin de précision / question à poser), envisagé le 6 septembre, reste à concevoir dans son ensemble — ce qui a été construit ce 7 septembre n'en couvre que deux (succès silencieux, échec avec avertissement), volontairement, en attendant un second cas concret qui justifierait la généralisation.
|
||||
- Tous les points laissés ouverts le 6 septembre (écran de gestion des consignes, catalogue commercial restreint, accumulation de plusieurs messages d'erreur, consignes transverses `CNS_Champ` NULL, distinction Saisie/Validation) restent entiers, aucun n'a été traité ce 7 septembre.
|
||||
|
||||
+248
-8
@@ -675,7 +675,12 @@
|
||||
.ligne-realisation.zebra-b{background:var(--zebra-bleu);}
|
||||
.ligne-realisation .col-rea{padding:0 8px 0 0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
|
||||
.col-rea-date{width:120px;flex:0 0 auto;}
|
||||
.col-rea-qui{width:40px;flex:0 0 auto;}
|
||||
/* 40px -> 90px (7 septembre 2026, nuit) : dimensionnée à l'origine pour un
|
||||
code à 3 caractères (REA_CodeUser, ex. "JMP"), devenue trop étroite
|
||||
depuis l'affichage de REA_NomAffiche (prénom en clair, ex. "Jean-Marc",
|
||||
"Matilda") — le texte était tronqué à 2-3 caractères par l'ellipsis
|
||||
ci-dessus (Jean-Marc : "n'affiche que 3 caractères"). */
|
||||
.col-rea-qui{width:90px;flex:0 0 auto;}
|
||||
.col-rea-action{width:190px;flex:0 0 auto;}
|
||||
.col-rea-comment{flex:1 1 auto;}
|
||||
#pied-realisations{
|
||||
@@ -710,6 +715,59 @@
|
||||
top:50%;bottom:auto;transform:translate(-50%,-50%);
|
||||
background:var(--rouge-suppression);color:#fff;
|
||||
}
|
||||
/* -------- Badge IA générique (7 septembre 2026, soir) --------
|
||||
Affiché en plein milieu de l'écran, "comme s'il s'agissait d'un message"
|
||||
(demande explicite de Jean-Marc) — même position que #toast.toast-erreur
|
||||
ci-dessus, même z-index (jamais affiché en même temps qu'un toast dans la
|
||||
pratique : masqué avant qu'un toast d'erreur éventuel n'apparaisse, voir
|
||||
validerFormulaireContact()). Volontairement générique, pas un habillage
|
||||
par agent/couleur (Jean-Marc a explicitement écarté cette idée : "si un
|
||||
jour un traitement fait appel à plusieurs agents à la suite, il ne
|
||||
faudrait pas que l'écran se transforme en sapin de Noël").
|
||||
Affiché UNIQUEMENT quand consignes_apercu.php répond que l'IA va
|
||||
réellement être sollicitée (voir validerFormulaireContact() plus bas,
|
||||
et moteur_consignes.php pour le mécanisme "_seraitDeclenchee") — jamais
|
||||
de façon systématique à chaque enregistrement, ni pour un simple
|
||||
enregistrement classique où le temps de traitement est imperceptible :
|
||||
décision ferme de Jean-Marc ("l'utilisateur pourrait s'affoler sur le
|
||||
coût systématique d'une insertion client"). Taille : 65px (7 septembre
|
||||
2026, nuit — plusieurs essais avec Jean-Marc, 130px puis 50px puis 25px
|
||||
jugés tour à tour trop gros, avant un retour à 65px une fois un souci de
|
||||
rafraîchissement d'écran écarté), confortablement au-dessus de la
|
||||
quarantaine de pixels repérée comme seuil de lisibilité du dessin
|
||||
(trois volutes fines) lors du tout premier test Playwright. Simple
|
||||
valeur CSS, aucune autre incidence. pointer-events:none (même principe
|
||||
que #micro-icone/#pastille-contacts sur l'écran de fond) : ce badge est
|
||||
purement informatif, jamais un obstacle au clic. */
|
||||
#badge-ia-actif{
|
||||
position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
|
||||
width:65px;height:65px;
|
||||
display:none;z-index:700;
|
||||
pointer-events:none;
|
||||
}
|
||||
#badge-ia-actif svg{display:block;width:100%;height:100%;}
|
||||
/* Ligne "nom de l'agent" sous le message (7 septembre 2026) : seulement
|
||||
pour le toast d'erreur, seulement quand la consigne en échec a un
|
||||
CNS_Agent renseigné — voir afficherToast(message, erreur, agent) et
|
||||
moteur_consignes.php. Italique pour la distinguer visuellement du
|
||||
message lui-même, légèrement moins contrastée (mais toujours lisible
|
||||
sur le fond rouge) plutôt qu'une simple répétition du même style. */
|
||||
#toast .toast-agent{
|
||||
margin-top:6px;font-style:italic;color:rgba(255,255,255,0.85);
|
||||
}
|
||||
/* Bouton de validation d'un toast bloquant (7 septembre 2026, avertissement
|
||||
IA) : ce toast-là ne disparaît pas tout seul (voir afficherToast()) — il
|
||||
faut un clic explicite de l'utilisateur pour le refermer, ce qui fait
|
||||
alors repasser la fiche du mode édition au mode vue (voir
|
||||
validerFormulaireContact()). Fond blanc/texte rouge plutôt qu'un simple
|
||||
texte cliquable, pour bien marquer que c'est une action et pas une
|
||||
information de plus. */
|
||||
#toast .toast-bouton{
|
||||
display:block;margin:12px auto 0;padding:6px 18px;
|
||||
background:#fff;color:var(--rouge-suppression);
|
||||
border:none;border-radius:4px;font-size:13px;font-weight:600;
|
||||
cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1044,6 +1102,41 @@ Créé par Mevelia Solutions">
|
||||
<!-- Toast -->
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Badge IA générique (7 septembre 2026, soir — voir commentaire CSS
|
||||
#badge-ia-actif ci-dessus pour le détail des décisions). Fourni par
|
||||
Jean-Marc (fichier MevelIA_loader_spin.svg) : pinwheel aux couleurs
|
||||
ASPIC (blanc cassé + orange de la marque), déjà pensé pour tourner en
|
||||
CSS pur et respecter prefers-reduced-motion — repris ici tel quel,
|
||||
sans aucune modification du dessin. -->
|
||||
<div id="badge-ia-actif" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-108 -108 216 216" width="216" height="216">
|
||||
<defs>
|
||||
<radialGradient id="badgeIaGlow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#101820" stop-opacity="0.55"/>
|
||||
<stop offset="55%" stop-color="#101820" stop-opacity="0.30"/>
|
||||
<stop offset="100%" stop-color="#101820" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<style>
|
||||
.mevelia-spin {
|
||||
transform-origin: 0px 0px;
|
||||
animation: mevelia-rotate 2.6s linear infinite;
|
||||
}
|
||||
@keyframes mevelia-rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mevelia-spin { animation: none; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle cx="0" cy="0" r="108" fill="url(#badgeIaGlow)"/>
|
||||
<g class="mevelia-spin">
|
||||
<g transform="rotate(100)"><path d="M 19.32,-5.18 L 20.10,-4.24 L 20.95,-3.26 L 21.80,-2.20 L 22.64,-1.05 L 23.44,0.17 L 24.20,1.48 L 24.91,2.87 L 25.55,4.35 L 26.11,5.91 L 26.59,7.54 L 26.99,9.25 L 27.28,11.02 L 27.47,12.86 L 27.55,14.75 L 27.51,16.70 L 27.35,18.68 L 27.06,20.70 L 26.63,22.75 L 26.07,24.81 L 25.36,26.88 L 24.51,28.96 L 23.52,31.02 L 22.37,33.06 L 21.07,35.07 L 19.63,37.04 L 18.03,38.96 L 16.29,40.82 L 14.39,42.61 L 12.36,44.31 L 10.18,45.92 L 7.87,47.43 L 5.42,48.82 L 2.84,50.09 L 0.15,51.22 L -2.66,52.21 L -5.58,53.05 L -8.59,53.73 L -11.69,54.24 L -14.88,54.57 L -18.13,54.71 L -21.44,54.67 L -24.80,54.43 L -28.20,53.98 L -31.62,53.33 L -35.06,52.47 L -38.49,51.39 L -41.91,50.09 L -45.30,48.58 L -48.65,46.84 L -51.94,44.89 L -55.17,42.72 L -58.31,40.33 L -61.35,37.72 L -64.29,34.91 L -67.10,31.88 L -69.77,28.66 L -72.28,25.24 L -74.63,21.64 L -76.80,17.85 L -78.78,13.89" fill="none" stroke="#FBF8F4" stroke-width="11.9" stroke-linecap="round" stroke-linejoin="round"/><circle cx="-78.78" cy="13.89" r="11" fill="#FBF8F4"/></g><g transform="rotate(220)"><path d="M 19.32,-5.18 L 20.10,-4.24 L 20.95,-3.26 L 21.80,-2.20 L 22.64,-1.05 L 23.44,0.17 L 24.20,1.48 L 24.91,2.87 L 25.55,4.35 L 26.11,5.91 L 26.59,7.54 L 26.99,9.25 L 27.28,11.02 L 27.47,12.86 L 27.55,14.75 L 27.51,16.70 L 27.35,18.68 L 27.06,20.70 L 26.63,22.75 L 26.07,24.81 L 25.36,26.88 L 24.51,28.96 L 23.52,31.02 L 22.37,33.06 L 21.07,35.07 L 19.63,37.04 L 18.03,38.96 L 16.29,40.82 L 14.39,42.61 L 12.36,44.31 L 10.18,45.92 L 7.87,47.43 L 5.42,48.82 L 2.84,50.09 L 0.15,51.22 L -2.66,52.21 L -5.58,53.05 L -8.59,53.73 L -11.69,54.24 L -14.88,54.57 L -18.13,54.71 L -21.44,54.67 L -24.80,54.43 L -28.20,53.98 L -31.62,53.33 L -35.06,52.47 L -38.49,51.39 L -41.91,50.09 L -45.30,48.58 L -48.65,46.84 L -51.94,44.89 L -55.17,42.72 L -58.31,40.33 L -61.35,37.72 L -64.29,34.91 L -67.10,31.88 L -69.77,28.66 L -72.28,25.24 L -74.63,21.64 L -76.80,17.85 L -78.78,13.89" fill="none" stroke="#FF5A1F" stroke-width="11.9" stroke-linecap="round" stroke-linejoin="round"/><circle cx="-78.78" cy="13.89" r="11" fill="#FF5A1F"/></g><g transform="rotate(340)"><path d="M 19.32,-5.18 L 20.10,-4.24 L 20.95,-3.26 L 21.80,-2.20 L 22.64,-1.05 L 23.44,0.17 L 24.20,1.48 L 24.91,2.87 L 25.55,4.35 L 26.11,5.91 L 26.59,7.54 L 26.99,9.25 L 27.28,11.02 L 27.47,12.86 L 27.55,14.75 L 27.51,16.70 L 27.35,18.68 L 27.06,20.70 L 26.63,22.75 L 26.07,24.81 L 25.36,26.88 L 24.51,28.96 L 23.52,31.02 L 22.37,33.06 L 21.07,35.07 L 19.63,37.04 L 18.03,38.96 L 16.29,40.82 L 14.39,42.61 L 12.36,44.31 L 10.18,45.92 L 7.87,47.43 L 5.42,48.82 L 2.84,50.09 L 0.15,51.22 L -2.66,52.21 L -5.58,53.05 L -8.59,53.73 L -11.69,54.24 L -14.88,54.57 L -18.13,54.71 L -21.44,54.67 L -24.80,54.43 L -28.20,53.98 L -31.62,53.33 L -35.06,52.47 L -38.49,51.39 L -41.91,50.09 L -45.30,48.58 L -48.65,46.84 L -51.94,44.89 L -55.17,42.72 L -58.31,40.33 L -61.35,37.72 L -64.29,34.91 L -67.10,31.88 L -69.77,28.66 L -72.28,25.24 L -74.63,21.64 L -76.80,17.85 L -78.78,13.89" fill="none" stroke="#FBF8F4" stroke-width="11.9" stroke-linecap="round" stroke-linejoin="round"/><circle cx="-78.78" cy="13.89" r="11" fill="#FBF8F4"/></g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* =========================================================================
|
||||
ASPIC — Écran "Liste des contacts" + panneau "Détail du contact"
|
||||
@@ -1418,16 +1511,52 @@ document.getElementById("menu-filtrer").addEventListener("click", ()=>{
|
||||
|
||||
// ---- Toast ----
|
||||
let toastTimer = null;
|
||||
function afficherToast(message, erreur){
|
||||
// Troisième paramètre "agent" (7 septembre 2026) : nom libre (CNS_Agent,
|
||||
// voir moteur_consignes.php) de l'agent rattaché à la consigne en échec,
|
||||
// remonté par contacts.php dans data.agent — affiché en italique sur une
|
||||
// ligne sous le message, uniquement pour le toast d'erreur et uniquement
|
||||
// quand une valeur est effectivement présente (une consigne sans agent
|
||||
// renseigné ne doit rien ajouter). Reconstruit le contenu du toast en deux
|
||||
// éléments plutôt qu'un simple textContent : nécessaire pour isoler le
|
||||
// style italique sur la seule ligne agent, sans toucher au message
|
||||
// principal ni à son white-space:pre-line hérité (voir CSS #toast).
|
||||
function afficherToast(message, erreur, agent, onValider){
|
||||
const toast = document.getElementById("toast");
|
||||
toast.textContent = message;
|
||||
toast.innerHTML = "";
|
||||
const ligneMessage = document.createElement("div");
|
||||
ligneMessage.textContent = message;
|
||||
toast.appendChild(ligneMessage);
|
||||
if(erreur && agent){
|
||||
const ligneAgent = document.createElement("div");
|
||||
ligneAgent.className = "toast-agent";
|
||||
ligneAgent.textContent = agent;
|
||||
toast.appendChild(ligneAgent);
|
||||
}
|
||||
// Variante erreur (6 septembre 2026) : centrée sur le panneau de saisie,
|
||||
// en rouge — voir usage dans validerFormulaireContact().
|
||||
toast.classList.toggle("toast-erreur", !!erreur);
|
||||
toast.style.display = "block";
|
||||
clearTimeout(toastTimer);
|
||||
if(onValider){
|
||||
// Toast bloquant (7 septembre 2026, avertissement IA — Jean-Marc : "je
|
||||
// souhaite que le message s'affiche et reste en place [...] et que
|
||||
// lorsque l'utilisateur clique, la fiche soit validée") : pas de
|
||||
// disparition automatique ici, l'utilisateur referme lui-même via ce
|
||||
// bouton, qui déclenche alors onValider (bascule en mode vue).
|
||||
const bouton = document.createElement("button");
|
||||
bouton.type = "button";
|
||||
bouton.className = "toast-bouton";
|
||||
bouton.textContent = "Valider";
|
||||
bouton.onclick = () => {
|
||||
toast.style.display = "none";
|
||||
toast.classList.remove("toast-erreur");
|
||||
onValider();
|
||||
};
|
||||
toast.appendChild(bouton);
|
||||
} else {
|
||||
toastTimer = setTimeout(()=>{ toast.style.display = "none"; }, 2600);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bouton micro (pilotage vocal) ----
|
||||
// Comportement réel (reconnaissance vocale) désormais dans micro.js,
|
||||
@@ -1721,7 +1850,7 @@ async function rendreRealisations(idContact){
|
||||
ligne.className = "ligne-realisation " + (index % 2 === 0 ? "zebra-a" : "zebra-b");
|
||||
ligne.innerHTML = `
|
||||
<div class="col-rea col-rea-date">${formaterDateHeure(r.REA_Date)}</div>
|
||||
<div class="col-rea col-rea-qui">${r.REA_CodeUser || ""}</div>
|
||||
<div class="col-rea col-rea-qui">${r.REA_NomAffiche || r.REA_CodeUser || ""}</div>
|
||||
<div class="col-rea col-rea-action">${r.ACT_Libelle || r.REA_Action || ""}</div>
|
||||
<div class="col-rea col-rea-comment">${r.REA_Comment || ""}</div>
|
||||
`;
|
||||
@@ -2085,6 +2214,7 @@ function fermerPanneauDetail(){
|
||||
}
|
||||
|
||||
const btnEnregistrer = document.getElementById("btn-enregistrer");
|
||||
const badgeIaActif = document.getElementById("badge-ia-actif");
|
||||
const MODES_VALIDES = ["vue", "edition", "suppression"];
|
||||
|
||||
// Bouton + placeholder du bloc Informations : le texte dépend du mode ET,
|
||||
@@ -2229,7 +2359,11 @@ async function validerFormulaireContact(){
|
||||
// contacts.php (POST/PUT) qui applique les consignes actives et renvoie,
|
||||
// le cas échéant, le message d'erreur tel quel — affiché ci-dessous sans
|
||||
// savoir à l'avance de quelle consigne il vient. Ajouter une consigne côté
|
||||
// base ne demande donc plus aucune modification ici.
|
||||
// base ne demande donc plus aucune modification ici. Depuis le 7 septembre
|
||||
// 2026, data.agent (CNS_Agent de la consigne en échec, facultatif) est lu
|
||||
// en même temps que data.error et transmis à afficherToast() pour la ligne
|
||||
// agent en italique — même principe : rien à modifier ici pour une future
|
||||
// consigne, qu'elle porte un agent ou non.
|
||||
const donnees = {};
|
||||
CHAMPS_TEXTE_MODIFIABLES.forEach(nomChamp=>{
|
||||
const el = panneauDetail.querySelector(
|
||||
@@ -2244,8 +2378,72 @@ async function validerFormulaireContact(){
|
||||
const commentaire = commentaireModification.value.trim();
|
||||
const enModification = !!contactCourant;
|
||||
|
||||
// Badge IA (7 septembre 2026, soir — voir #badge-ia-actif dans le CSS pour
|
||||
// le détail des décisions de Jean-Marc). Après deux allers-retours avec lui,
|
||||
// AUCUNE condition n'est plus testée en dur ici ("Si tu introduis des tests
|
||||
// en dur pour faire apparaître ou pas ce visuel, on sort de la logique
|
||||
// initiale") : on interroge consignes_apercu.php, qui pose la même question
|
||||
// au jumeau "_seraitDeclenchee" de la fonction elle-même (voir en-tête de
|
||||
// consignes_fonctions.php et moteur_consignes.php) — la seule et unique
|
||||
// définition de "l'IA va-t-elle être sollicitée pour ces valeurs". Ajouter
|
||||
// une future consigne IA sur d'autres champs ne demandera donc toujours
|
||||
// aucune modification ici, tant qu'elle porte son propre jumeau
|
||||
// "_seraitDeclenchee" et FON_VisuelAttenteIA = 'O' en base.
|
||||
//
|
||||
// Appelé AVANT l'enregistrement réel (POST/PUT ci-dessous) : c'est ce qui
|
||||
// permet au badge d'être déjà visible pendant l'attente de cet
|
||||
// enregistrement, potentiellement plus lent quand l'IA est sollicitée.
|
||||
// Repli silencieux sur "pas de badge" en cas de souci réseau sur cet appel
|
||||
// lui-même — un aléa sur ce simple indicateur visuel ne doit jamais
|
||||
// empêcher l'enregistrement du contact de continuer normalement.
|
||||
let declenchementIaPrevu = false;
|
||||
try {
|
||||
const reponseApercu = await fetch("consignes_apercu.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
contexte: "Contacts",
|
||||
portee: enModification ? "Modification" : "Création",
|
||||
donnees
|
||||
})
|
||||
});
|
||||
if(reponseApercu.ok){
|
||||
const dataApercu = await reponseApercu.json();
|
||||
declenchementIaPrevu = !!(dataApercu && dataApercu.declenche);
|
||||
}
|
||||
} catch(erreurApercu){
|
||||
console.error("Erreur consignes_apercu.php :", erreurApercu);
|
||||
}
|
||||
|
||||
// Bouton désactivé dans tous les cas (protection simple contre un double
|
||||
// clic), mais SANS aucun changement de libellé pour un enregistrement
|
||||
// classique — décision ferme de Jean-Marc : "en traitement simple de
|
||||
// validation, le temps de traitement est imperceptible, aucune raison
|
||||
// d'afficher quoi que ce soit". Le badge, bien plus visible (plein milieu
|
||||
// de l'écran), n'apparaît que si consignes_apercu.php l'annonce — JAMAIS de
|
||||
// façon systématique : "hors de question d'afficher systématiquement le
|
||||
// badge IA, l'utilisateur pourrait s'affoler sur le coût systématique d'une
|
||||
// insertion client". Restauré explicitement à chaque sortie (erreur réseau,
|
||||
// erreur consigne, ou succès juste avant le changement de mode plus bas)
|
||||
// plutôt que dans un bloc finally, pour rester à l'endroit exact où chaque
|
||||
// cas se termine.
|
||||
btnEnregistrer.disabled = true;
|
||||
if(declenchementIaPrevu) badgeIaActif.style.display = "block";
|
||||
const restaurerBouton = () => {
|
||||
btnEnregistrer.disabled = false;
|
||||
badgeIaActif.style.display = "none";
|
||||
};
|
||||
|
||||
try {
|
||||
let idContact;
|
||||
// Avertissements (7 septembre 2026, nuit — voir moteur_consignes.php,
|
||||
// clé réservée "_avertissement") : messages génériques que le serveur
|
||||
// peut renvoyer en plus d'un enregistrement réussi (ex. l'IA a bien été
|
||||
// sollicitée mais n'a rien pu en tirer). Affichés plus bas, une fois
|
||||
// l'enregistrement confirmé — ce code ne sait pas, et n'a pas besoin de
|
||||
// savoir, quelle consigne a pu en produire.
|
||||
let avertissements = [];
|
||||
|
||||
if(enModification){
|
||||
idContact = contactCourant.CNT_Incr;
|
||||
const reponse = await fetch("contacts.php?id=" + encodeURIComponent(idContact), {
|
||||
@@ -2258,9 +2456,11 @@ async function validerFormulaireContact(){
|
||||
console.error("Erreur PUT contacts.php :", reponse.status, data ? data.error : texte);
|
||||
// Message précis du serveur (ex. une consigne en échec) si présent,
|
||||
// message générique seulement en dernier recours (ex. panne réseau).
|
||||
afficherToast((data && data.error) || "Erreur : impossible d'enregistrer les modifications.", true);
|
||||
afficherToast((data && data.error) || "Erreur : impossible d'enregistrer les modifications.", true, data && data.agent);
|
||||
restaurerBouton();
|
||||
return;
|
||||
}
|
||||
avertissements = (data && data.avertissements) || [];
|
||||
} else {
|
||||
const reponse = await fetch("contacts.php", {
|
||||
method: "POST",
|
||||
@@ -2270,10 +2470,12 @@ async function validerFormulaireContact(){
|
||||
const {json: data, texte} = await lireReponse(reponse);
|
||||
if(!reponse.ok || !data || data.error || !data.id){
|
||||
console.error("Erreur POST contacts.php :", reponse.status, data ? data.error : texte);
|
||||
afficherToast((data && data.error) || "Erreur : impossible de créer ce contact.", true);
|
||||
afficherToast((data && data.error) || "Erreur : impossible de créer ce contact.", true, data && data.agent);
|
||||
restaurerBouton();
|
||||
return;
|
||||
}
|
||||
idContact = data.id;
|
||||
avertissements = (data && data.avertissements) || [];
|
||||
}
|
||||
|
||||
// Contact bien enregistré à ce stade. La journalisation de la réalisation
|
||||
@@ -2297,6 +2499,13 @@ async function validerFormulaireContact(){
|
||||
: "Contact créé, mais l'historique n'a pas pu être journalisé.");
|
||||
}
|
||||
|
||||
// Bouton réactivé et badge masqué avant le changement de mode ci-dessous :
|
||||
// afficherDetailPourId() rebascule le panneau en mode "vue" (ou "edition",
|
||||
// voir ci-dessous) et réactive déjà ce bouton à sa façon
|
||||
// (mettreAJourLibellesActionPanneau()), donc rien de contradictoire à le
|
||||
// faire ici en plus, juste avant.
|
||||
restaurerBouton();
|
||||
|
||||
// Retour sur le panneau de visualisation du contact (25 août 2026, pour
|
||||
// modification ET création) plutôt que fermeture/retour liste — meilleure
|
||||
// façon de voir que l'enregistrement a bien été pris en compte. Pas de
|
||||
@@ -2305,13 +2514,44 @@ async function validerFormulaireContact(){
|
||||
// arrière-plan) avant de réafficher le détail : la barre de position et
|
||||
// la navigation PgUp/PgDn du panneau détail doivent refléter le tri/
|
||||
// filtre à jour de contactsAffiches, pas son état d'avant enregistrement.
|
||||
//
|
||||
// S'il y a un avertissement, on reste d'abord en mode édition au lieu de
|
||||
// repasser en vue, pour que l'utilisateur puisse corriger tout de suite
|
||||
// le champ resté incomplet (ex. code postal non déduit). On appelle
|
||||
// quand même afficherDetailPourId (et non un simple maintien d'affichage)
|
||||
// afin que contactCourant soit bien rafraîchi avec le contact tel
|
||||
// qu'enregistré côté serveur (id inclus) : indispensable en création pour
|
||||
// qu'un second clic sur "Enregistrer" fasse un PUT sur ce contact et non
|
||||
// un nouveau POST (pas de doublon).
|
||||
await rendreListe();
|
||||
await afficherDetailPourId(idContact, "vue");
|
||||
await afficherDetailPourId(idContact, avertissements.length ? "edition" : "vue");
|
||||
|
||||
// Avertissement(s) du serveur (voir plus haut) : affiché(s) une fois le
|
||||
// panneau réaffiché en mode édition ci-dessus, plusieurs messages étant
|
||||
// regroupés en un seul toast, un par ligne (#toast a déjà
|
||||
// white-space:pre-line pour un CNS_MessageErreur multi-lignes, réutilisé
|
||||
// ici tel quel). Rendu en erreur (true) bien que l'enregistrement du
|
||||
// contact ait réussi : c'est un échec de l'IA (ex. code postal non
|
||||
// déduit) qui appelle une action de l'utilisateur.
|
||||
// Toast bloquant (7 septembre 2026, demande explicite de Jean-Marc :
|
||||
// "je souhaite que le message s'affiche et reste en place [...] et que
|
||||
// lorsque l'utilisateur clique, la fiche soit validée") : pas de
|
||||
// disparition automatique — la fiche reste visuellement "en attente"
|
||||
// (mode édition) tant que l'utilisateur n'a pas cliqué sur "Valider"
|
||||
// dans le toast, ce qui ne fait alors que rebasculer l'affichage en mode
|
||||
// vue (le contact est déjà bel et bien enregistré depuis plus haut,
|
||||
// avertissement ou non — ce bouton ne redéclenche aucun enregistrement).
|
||||
if(avertissements.length){
|
||||
afficherToast(avertissements.join("\n"), true, undefined, () => {
|
||||
basculerModeDetail("vue");
|
||||
});
|
||||
}
|
||||
} catch(erreur){
|
||||
console.error("Erreur d'enregistrement :", erreur);
|
||||
afficherToast(enModification
|
||||
? "Erreur : impossible d'enregistrer les modifications."
|
||||
: "Erreur : impossible de créer ce contact.", true);
|
||||
restaurerBouton();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,7 +432,9 @@
|
||||
.then(function(donnees){
|
||||
const liste = (donnees && donnees.agents) || [];
|
||||
if(liste.length === 0) throw new Error("liste vide");
|
||||
agentsDisponibles = liste.map(function(a){ return construireAgent(a.AGN_Nom, a.AGN_Variantes); });
|
||||
// AGN_Nom renommée AGN_Prenom en base (7 septembre 2026, nuit) — voir
|
||||
// agents.php, qui renvoie désormais cette clé sous son nouveau nom.
|
||||
agentsDisponibles = liste.map(function(a){ return construireAgent(a.AGN_Prenom, a.AGN_Variantes); });
|
||||
agentsPrets = true;
|
||||
})
|
||||
.catch(function(){
|
||||
|
||||
+291
-28
@@ -12,10 +12,19 @@
|
||||
* à un champ ou une consigne précis — tout vient de la base.
|
||||
*
|
||||
* Portée de cette première version, volontairement limitée :
|
||||
* - Un seul champ à la fois (CNS_Champ NULL — consigne transverse à la fiche
|
||||
* entière, ex. futur contrôle de doublon — n'est PAS encore géré ; ces
|
||||
* lignes sont ignorées pour l'instant, à reprendre quand un vrai cas se
|
||||
* présentera).
|
||||
* - CNS_Champ NULL (consigne transverse à la fiche entière, ex. futur
|
||||
* contrôle de doublon) n'est PAS encore géré ; ces lignes sont ignorées
|
||||
* pour l'instant, à reprendre quand un vrai cas se présentera. En revanche
|
||||
* CNS_Champ PEUT désormais contenir PLUSIEURS noms de champs séparés par
|
||||
* une virgule (ex. "CNT_AdrVille,CNT_AdrCP") — extension du 7 septembre
|
||||
* 2026 (soir), nécessaire pour une consigne dont la réponse dépend de
|
||||
* plusieurs champs à la fois (ex. déduire le code postal à partir de la
|
||||
* ville, voir consigne_completer_code_postal() dans
|
||||
* consignes_fonctions.php). Une consigne à un seul champ garde exactement
|
||||
* le même contrat qu'avant (fonction appelée avec une simple chaîne) ; une
|
||||
* consigne à plusieurs champs appelle sa fonction avec un tableau associatif
|
||||
* [nom_du_champ => valeur] et attend en retour, pour un Traitement, ce même
|
||||
* type de tableau (valeurs mises à jour) — voir les deux boucles ci-dessous.
|
||||
* - CNS_Moment n'est PAS distingué ici : temps réel (Saisie) abandonné pour
|
||||
* cette version (décision de Jean-Marc, 6 septembre 2026 — priorité à la
|
||||
* robustesse plutôt qu'au retour immédiat à l'écran). Toute consigne
|
||||
@@ -28,22 +37,88 @@
|
||||
* moments" (Création et Modification), à côté de valeurs plus précises
|
||||
* ('Création' ou 'Modification' seules seraient possibles plus tard pour une
|
||||
* règle d'immuabilité — voir Réflexions stratégiques).
|
||||
*
|
||||
* CNS_Agent (7 septembre 2026) : nom libre (varchar, pas de clé étrangère
|
||||
* vers ASP_Agents) permettant de rattacher une consigne à un agent — utile
|
||||
* en particulier pour les prochaines consignes de test qui simuleront des
|
||||
* comportements par agent. Champ facultatif, remonté via le paramètre par
|
||||
* référence $agentEchec plutôt qu'en changeant le type de retour de la
|
||||
* fonction (qui reste un simple string|null, pour ne pas casser le contrat
|
||||
* déjà en place) : seul contacts.php lit ce paramètre pour l'ajouter à sa
|
||||
* réponse JSON, le front-end (afficherToast) l'affiche en italique sous le
|
||||
* message quand il est présent.
|
||||
*
|
||||
* Journalisation des Traitements IA (7 septembre 2026, soir) : après le
|
||||
* premier essai réel de consigne_completer_code_postal(), Jean-Marc a demandé
|
||||
* de pouvoir tracer QUAND et POURQUOI l'IA a été sollicitée (bilan de
|
||||
* consommation de tokens, entre autres). Un Traitement à PLUSIEURS champs
|
||||
* (voir ci-dessus) peut donc désormais glisser, dans le tableau qu'il
|
||||
* retourne, une clé réservée "_realisation" — jamais un vrai nom de champ
|
||||
* ASPIC (tous commencent par un préfixe majuscule type "CNT_") donc aucun
|
||||
* risque de collision. Cette clé est retirée par le moteur avant d'écrire le
|
||||
* reste dans $data (voir boucle des Traitements ci-dessous), et collectée
|
||||
* dans le nouveau paramètre par référence $journalisations. Volontairement
|
||||
* PAS d'écriture directe dans ASP_Realisations ici : ce fichier n'a pas
|
||||
* connaissance de CNT_Incr (inexistant tant que l'INSERT n'a pas eu lieu, en
|
||||
* création) — c'est contacts.php qui journalise réellement, une fois
|
||||
* l'identifiant du contact connu (voir ce fichier).
|
||||
*
|
||||
* Badge IA générique côté écran (7 septembre 2026, soir, suite) : le premier
|
||||
* jet de ce badge reposait sur une condition écrite en dur dans
|
||||
* liste_contacts.html (ville renseignée, code postal vide) — Jean-Marc a
|
||||
* fait remarquer à juste titre que c'est exactement la même condition que la
|
||||
* garde en tête de consigne_completer_code_postal(), et que dupliquer cette
|
||||
* condition côté écran allait à l'encontre du chantier des consignes (une
|
||||
* nouvelle consigne IA aurait demandé de retoucher le JS). Nouvelle
|
||||
* convention à la place : une fonction de Traitement porteuse de
|
||||
* FON_VisuelAttenteIA = 'O' (ASP_Fonctions — propriété de la FONCTION,
|
||||
* remarque de Jean-Marc : c'est elle qui interroge une IA, pas la consigne
|
||||
* qui l'utilise) DOIT avoir un jumeau nommé "<FON_NomPHP>_seraitDeclenchee"
|
||||
* dans consignes_fonctions.php, signature (array $valeurs): bool — jamais
|
||||
* d'appel réseau, juste la même garde que la vraie fonction (qui l'appelle en
|
||||
* premier, voir consigne_completer_code_postal()) : la condition n'existe
|
||||
* ainsi qu'à un seul endroit. Voir auMoinsUneConsigneIaSeraitDeclenchee()
|
||||
* ci-dessous, utilisée par le nouveau point d'accès consignes_apercu.php —
|
||||
* l'écran l'interroge une fois, juste avant l'enregistrement réel, pour
|
||||
* savoir s'il doit afficher le badge pendant l'attente qui suit.
|
||||
*
|
||||
* Hiérarchisation Traitements de cohérence / Traitements IA (7 septembre
|
||||
* 2026, nuit) : test réel de Jean-Marc (mail invalide + ville sans code
|
||||
* postal) — l'ordre d'origine (TOUS les Traitements, puis TOUS les Tests)
|
||||
* faisait sonner l'IA (coût réel, appel réseau) AVANT que le Test MAIL_VALIDE
|
||||
* échoue et bloque l'enregistrement ; le contact n'étant jamais créé, cet
|
||||
* appel IA n'était même pas journalisable (pas de CNT_Incr) — perdu, coût et
|
||||
* trace. Pire : en resaisissant pour corriger le mail, un DEUXIÈME appel IA
|
||||
* partait pour le même contact. Jean-Marc : "il faut qu'on hiérarchise les
|
||||
* traitements : d'abord les traitements de cohérence, après les traitements
|
||||
* par IA". Nouvel ordre d'exécution, réutilisant FON_VisuelAttenteIA comme
|
||||
* seule frontière (déjà la bonne propriété : "cette fonction interroge un
|
||||
* service externe") :
|
||||
* 1. Traitements de cohérence (FON_VisuelAttenteIA != 'O') — silencieux,
|
||||
* gratuits, locaux, comme avant.
|
||||
* 2. Tests, sur les valeurs déjà mises en cohérence ci-dessus — premier
|
||||
* échec = arrêt immédiat, AVANT tout appel IA.
|
||||
* 3. Traitements IA (FON_VisuelAttenteIA = 'O') — atteints seulement si
|
||||
* tous les Tests sont passés : un enregistrement voué à l'échec ne
|
||||
* déclenche donc plus jamais d'appel IA.
|
||||
* Limite assumée, à revoir le jour où un vrai cas se présentera : un Test qui
|
||||
* porterait sur un champ produit par un Traitement IA (ex. valider le format
|
||||
* d'un code postal déduit par l'IA) verrait la valeur D'AVANT cet appel IA,
|
||||
* puisque les Tests s'exécutent avant l'étape 3 — aucune consigne actuelle
|
||||
* n'est dans ce cas.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param PDO $pdo Connexion déjà ouverte (voir db.php).
|
||||
* @param string $contexte Ex. 'Contacts' (ASP_Consignes.CNS_Contexte).
|
||||
* @param string $portee 'Création' ou 'Modification'.
|
||||
* @param array $data Données du formulaire, par référence — les
|
||||
* traitements silencieux (ex. majuscules) modifient
|
||||
* directement ce tableau.
|
||||
* @return string|null Message d'erreur du premier test en échec (déjà le
|
||||
* texte à renvoyer tel quel au front-end), ou null
|
||||
* si tout est passé.
|
||||
* Charge les consignes actives d'un contexte/portée donnés, avec leur
|
||||
* fonction associée — factorisé ici (7 septembre 2026, soir) car utilisé à
|
||||
* la fois par appliquerConsignes() ci-dessous et par
|
||||
* auMoinsUneConsigneIaSeraitDeclenchee() : une seule requête à faire évoluer
|
||||
* si la jointure ASP_Consignes/ASP_Fonctions change un jour.
|
||||
*/
|
||||
function appliquerConsignes(PDO $pdo, string $contexte, string $portee, array &$data): ?string
|
||||
function chargerConsignesActives(PDO $pdo, string $contexte, string $portee): array
|
||||
{
|
||||
$sql = "SELECT c.CNS_Champ, c.CNS_Nature, c.CNS_MessageErreur, f.FON_NomPHP
|
||||
$sql = "SELECT c.CNS_Champ, c.CNS_Nature, c.CNS_MessageErreur, c.CNS_Agent,
|
||||
f.FON_NomPHP, f.FON_VisuelAttenteIA
|
||||
FROM ASP_Consignes c
|
||||
LEFT JOIN ASP_Fonctions f
|
||||
ON f.FON_Nom = c.CNS_Fonction AND f.FON_Active = 'O'
|
||||
@@ -54,27 +129,215 @@ function appliquerConsignes(PDO $pdo, string $contexte, string $portee, array &$
|
||||
ORDER BY c.CNS_Incr";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$contexte, $portee]);
|
||||
$consignes = $stmt->fetchAll();
|
||||
|
||||
// Traitements d'abord (silencieux, jamais de message) : une consigne de
|
||||
// test lue ensuite doit voir la valeur déjà mise en forme (ex. le nom
|
||||
// déjà en majuscules avant de vérifier qu'il n'est pas vide).
|
||||
foreach ($consignes as $c) {
|
||||
if ($c['CNS_Nature'] !== 'Traitement') continue;
|
||||
if (!$c['FON_NomPHP'] || !function_exists($c['FON_NomPHP'])) continue;
|
||||
$valeurActuelle = (string)($data[$c['CNS_Champ']] ?? '');
|
||||
$data[$c['CNS_Champ']] = call_user_func($c['FON_NomPHP'], $valeurActuelle);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// Puis les tests, sur les valeurs déjà transformées ci-dessus.
|
||||
/**
|
||||
* Est-ce qu'au moins une consigne de Traitement porteuse de
|
||||
* FON_VisuelAttenteIA = 'O' se déclencherait réellement pour ces données ?
|
||||
* Jamais d'appel réseau ici (voir commentaire d'en-tête) : chaque fonction
|
||||
* concernée est interrogée via son jumeau "_seraitDeclenchee", pas la
|
||||
* fonction réelle. Utilisée par consignes_apercu.php pour piloter le badge
|
||||
* IA générique côté écran — un simple booléen, jamais le détail de quelle
|
||||
* consigne précisément (le badge reste volontairement générique, voir
|
||||
* réflexions avec Jean-Marc : pas de couleur/identité par agent ici).
|
||||
*/
|
||||
function auMoinsUneConsigneIaSeraitDeclenchee(PDO $pdo, string $contexte, string $portee, array $data): bool
|
||||
{
|
||||
$consignes = chargerConsignesActives($pdo, $contexte, $portee);
|
||||
foreach ($consignes as $c) {
|
||||
if ($c['CNS_Nature'] !== 'Traitement') continue;
|
||||
if (($c['FON_VisuelAttenteIA'] ?? 'N') !== 'O') continue;
|
||||
if (!$c['FON_NomPHP']) continue;
|
||||
$nomApercu = $c['FON_NomPHP'] . '_seraitDeclenchee';
|
||||
if (!function_exists($nomApercu)) continue;
|
||||
|
||||
$champs = array_map('trim', explode(',', $c['CNS_Champ']));
|
||||
$valeurs = [];
|
||||
foreach ($champs as $champ) {
|
||||
$valeurs[$champ] = (string)($data[$champ] ?? '');
|
||||
}
|
||||
if (call_user_func($nomApercu, $valeurs)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute une liste de consignes de Traitement (déjà filtrée par l'appelant)
|
||||
* sur $data, dans l'ordre reçu. Factorisé (7 septembre 2026, nuit) car
|
||||
* désormais appelé deux fois par appliquerConsignes() — une fois pour les
|
||||
* Traitements de cohérence, une fois pour les Traitements IA — avec
|
||||
* exactement la même mécanique à champ unique/multiple et la même collecte
|
||||
* de "_realisation" dans les deux cas.
|
||||
*
|
||||
* Provenance de l'agent journalisé (7 septembre 2026, nuit — suite) : Jean-
|
||||
* Marc a repéré que consigne_completer_code_postal() inventait elle-même son
|
||||
* agent ("Mat", en dur dans le code, sans lien avec aucune table) et a fait
|
||||
* remarquer que CNS_Agent existe déjà dans ASP_Consignes — exactement ce qui
|
||||
* alimente déjà $agentEchec pour un Test en échec (voir plus bas). Même
|
||||
* principe appliqué ici : l'agent réellement journalisé est TOUJOURS celui
|
||||
* de CNS_Agent quand la consigne en porte un (nom en clair, tronqué à 15
|
||||
* caractères pour tenir dans REA_CodeUser — voir suggestion SQL associée),
|
||||
* jamais celui que la fonction aurait pu proposer elle-même. Une fonction de
|
||||
* Traitement n'a donc plus aucune raison d'inventer un nom d'agent dans son
|
||||
* "_realisation" — au mieux une valeur de repli si jamais CNS_Agent était
|
||||
* laissé vide par erreur.
|
||||
*
|
||||
* Clé réservée "_avertissement" (7 septembre 2026, nuit — suite) : Jean-Marc,
|
||||
* testant "Paris" (ville sans code postal unique, découpée en
|
||||
* arrondissements), a remarqué qu'un appel IA infructueux ne laissait AUCUNE
|
||||
* trace, ni à l'écran ni dans les réalisations — "il y a un appel fait à
|
||||
* l'IA et pas de trace dans les réalisations, ce qui n'est pas normal".
|
||||
* "_realisation" couvre déjà l'historique (voir consigne_completer_code_postal(),
|
||||
* désormais journalisée aussi en cas d'échec) ; "_avertissement" couvre
|
||||
* l'AUTRE moitié du problème, le retour immédiat à l'écran — sans lui,
|
||||
* l'utilisateur ne voit rien se passer alors que l'IA a bien réfléchi et a
|
||||
* sciemment renoncé. Contrat : une simple chaîne (le message tel quel,
|
||||
* déjà formulé pour l'utilisateur), collectée ici de la même façon que
|
||||
* "_realisation", jamais écrite dans $data.
|
||||
*/
|
||||
function executerTraitements(array $consignesTraitement, array &$data, array &$journalisations, array &$avertissements): void
|
||||
{
|
||||
foreach ($consignesTraitement as $c) {
|
||||
if (!$c['FON_NomPHP'] || !function_exists($c['FON_NomPHP'])) continue;
|
||||
$champs = array_map('trim', explode(',', $c['CNS_Champ']));
|
||||
if (count($champs) === 1) {
|
||||
// Contrat historique, inchangé : une chaîne en entrée, une
|
||||
// chaîne en sortie.
|
||||
$valeurActuelle = (string)($data[$champs[0]] ?? '');
|
||||
$data[$champs[0]] = call_user_func($c['FON_NomPHP'], $valeurActuelle);
|
||||
} else {
|
||||
// Consigne à plusieurs champs (7 septembre 2026, voir
|
||||
// commentaire d'en-tête) : un tableau associatif dans les deux
|
||||
// sens, pour que la fonction sache quel champ correspond à
|
||||
// quelle valeur sans dépendre de l'ordre dans CNS_Champ.
|
||||
$valeursActuelles = [];
|
||||
foreach ($champs as $champ) {
|
||||
$valeursActuelles[$champ] = (string)($data[$champ] ?? '');
|
||||
}
|
||||
$valeursMisesAJour = call_user_func($c['FON_NomPHP'], $valeursActuelles);
|
||||
// Clé réservée "_realisation" (7 septembre 2026, voir commentaire
|
||||
// d'en-tête) : jamais écrite dans $data, juste collectée à part.
|
||||
if (isset($valeursMisesAJour['_realisation'])) {
|
||||
$realisation = $valeursMisesAJour['_realisation'];
|
||||
// L'agent vient de la consigne (CNS_Agent), jamais de la
|
||||
// fonction — voir commentaire ci-dessus. Repli sur ce que la
|
||||
// fonction avait proposé, puis sur 'IA' générique, seulement
|
||||
// si CNS_Agent est vide (ne devrait plus arriver une fois la
|
||||
// consigne correctement renseignée).
|
||||
$agentConsigne = trim((string)($c['CNS_Agent'] ?? ''));
|
||||
$realisation['agent'] = $agentConsigne !== ''
|
||||
? mb_substr($agentConsigne, 0, 15, 'UTF-8')
|
||||
: ($realisation['agent'] ?? 'IA');
|
||||
$journalisations[] = $realisation;
|
||||
unset($valeursMisesAJour['_realisation']);
|
||||
}
|
||||
// Clé réservée "_avertissement" (voir commentaire ci-dessus) :
|
||||
// même principe que "_realisation", jamais écrite dans $data.
|
||||
if (isset($valeursMisesAJour['_avertissement'])) {
|
||||
$avertissements[] = (string)$valeursMisesAJour['_avertissement'];
|
||||
unset($valeursMisesAJour['_avertissement']);
|
||||
}
|
||||
foreach ($valeursMisesAJour as $champ => $valeur) {
|
||||
$data[$champ] = $valeur;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PDO $pdo Connexion déjà ouverte (voir db.php).
|
||||
* @param string $contexte Ex. 'Contacts' (ASP_Consignes.CNS_Contexte).
|
||||
* @param string $portee 'Création' ou 'Modification'.
|
||||
* @param array $data Données du formulaire, par référence —
|
||||
* les traitements silencieux (ex.
|
||||
* majuscules) modifient directement ce
|
||||
* tableau.
|
||||
* @param string|null $agentEchec Sortie par référence : CNS_Agent de la
|
||||
* consigne dont le test a échoué (ou null
|
||||
* si tout est passé, ou si la consigne en
|
||||
* échec n'a pas d'agent renseigné).
|
||||
* @param array|null $journalisations Sortie par référence : liste des
|
||||
* entrées ['action'=>ACT_Code,
|
||||
* 'agent'=>CNS_Agent de la consigne
|
||||
* (nom en clair, tronqué à 15 caractères
|
||||
* — voir executerTraitements() ci-dessus),
|
||||
* 'commentaire'=>...] glissées par les
|
||||
* Traitements à plusieurs champs qui l'ont
|
||||
* demandé (voir commentaire d'en-tête) —
|
||||
* tableau vide si aucun (jamais null en
|
||||
* sortie, seul le type d'entrée est
|
||||
* nullable — voir note ci-dessous). À
|
||||
* journaliser dans ASP_Realisations par
|
||||
* l'appelant.
|
||||
* @param array|null $avertissements Sortie par référence : messages à
|
||||
* afficher immédiatement à l'écran
|
||||
* (voir "_avertissement" dans
|
||||
* executerTraitements() ci-dessus) —
|
||||
* tableau vide si aucun. À afficher par
|
||||
* l'appelant (ex. un toast par message),
|
||||
* indépendamment de $journalisations qui
|
||||
* couvre l'historique, pas l'écran.
|
||||
* @return string|null Message d'erreur du premier test en
|
||||
* échec (déjà le texte à renvoyer tel
|
||||
* quel au front-end), ou null si tout est
|
||||
* passé.
|
||||
*/
|
||||
function appliquerConsignes(PDO $pdo, string $contexte, string $portee, array &$data, ?string &$agentEchec = null, ?array &$journalisations = null, ?array &$avertissements = null): ?string
|
||||
{
|
||||
// Type nullable en entrée (comme $agentEchec ci-dessus) : un appelant qui
|
||||
// passe une variable non initialisée par référence la fait exister à
|
||||
// null, ce qu'un type "array" strict refuserait (TypeError) — repéré à
|
||||
// l'usage le 7 septembre 2026 en testant ce paramètre. Toujours remis à
|
||||
// un tableau (vide ou rempli) avant de sortir de la fonction.
|
||||
$agentEchec = null;
|
||||
$journalisations = [];
|
||||
$avertissements = [];
|
||||
|
||||
$consignes = chargerConsignesActives($pdo, $contexte, $portee);
|
||||
|
||||
// Étape 1 : Traitements de cohérence (silencieux, jamais de message,
|
||||
// jamais d'appel réseau) — une consigne de test lue ensuite doit voir la
|
||||
// valeur déjà mise en forme (ex. le nom déjà en majuscules avant de
|
||||
// vérifier qu'il n'est pas vide).
|
||||
$traitementsCoherence = array_filter($consignes, function (array $c): bool {
|
||||
return $c['CNS_Nature'] === 'Traitement' && ($c['FON_VisuelAttenteIA'] ?? 'N') !== 'O';
|
||||
});
|
||||
executerTraitements($traitementsCoherence, $data, $journalisations, $avertissements);
|
||||
|
||||
// Étape 2 : les Tests, sur les valeurs déjà mises en cohérence ci-dessus
|
||||
// (voir commentaire d'en-tête, 7 septembre 2026, nuit) : un premier échec
|
||||
// arrête tout ICI, avant la moindre sollicitation d'IA — c'est tout
|
||||
// l'objet de la réorganisation de ce soir.
|
||||
foreach ($consignes as $c) {
|
||||
if ($c['CNS_Nature'] !== 'Test') continue;
|
||||
if (!$c['FON_NomPHP'] || !function_exists($c['FON_NomPHP'])) continue;
|
||||
$valeur = (string)($data[$c['CNS_Champ']] ?? '');
|
||||
if (!call_user_func($c['FON_NomPHP'], $valeur)) {
|
||||
$champs = array_map('trim', explode(',', $c['CNS_Champ']));
|
||||
if (count($champs) === 1) {
|
||||
$valeur = (string)($data[$champs[0]] ?? '');
|
||||
$ok = call_user_func($c['FON_NomPHP'], $valeur);
|
||||
} else {
|
||||
$valeurs = [];
|
||||
foreach ($champs as $champ) {
|
||||
$valeurs[$champ] = (string)($data[$champ] ?? '');
|
||||
}
|
||||
$ok = call_user_func($c['FON_NomPHP'], $valeurs);
|
||||
}
|
||||
if (!$ok) {
|
||||
$agentEchec = $c['CNS_Agent'] ?: null;
|
||||
return $c['CNS_MessageErreur'] ?: 'Consigne non respectée.';
|
||||
}
|
||||
}
|
||||
|
||||
// Étape 3 : Traitements IA — atteints seulement si tous les Tests sont
|
||||
// passés (voir commentaire d'en-tête) : un enregistrement voué à échouer
|
||||
// ne déclenche donc plus jamais d'appel IA facturé pour rien.
|
||||
$traitementsIA = array_filter($consignes, function (array $c): bool {
|
||||
return $c['CNS_Nature'] === 'Traitement' && ($c['FON_VisuelAttenteIA'] ?? 'N') === 'O';
|
||||
});
|
||||
executerTraitements($traitementsIA, $data, $journalisations, $avertissements);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+36
-8
@@ -20,6 +20,27 @@
|
||||
* `aspic.php`) — le champ REA_CodeUser du corps de la requête POST n'est
|
||||
* plus utilisé et ne peut plus être falsifié par le client. Le "JMP" en dur
|
||||
* précédent a été supprimé (n'avait de sens que faute de session).
|
||||
*
|
||||
* REA_NomAffiche (7 septembre 2026, nuit) : Jean-Marc voulait voir "Jean-Marc"
|
||||
* plutôt que "JMP" dans l'historique d'un contact. Deux idées écartées en
|
||||
* cours de route : renommer USR_Code lui-même (trop structurant, utilisé
|
||||
* partout — session, REA_CodeUser, OPE_CodeUser...), et une jointure
|
||||
* ASP_Users refaite à chaque affichage (Jean-Marc : "comment vas-tu faire
|
||||
* pour les agents qui sont dans une autre base ?" — une jointure suppose que
|
||||
* tout vit dans la même base, hypothèse fragile). Solution retenue, dans les
|
||||
* mots de Jean-Marc : "si on stocke le prénom de l'utilisateur d'un côté et
|
||||
* le prénom de l'agent en dur on est cool" — un INSTANTANÉ pris UNE SEULE
|
||||
* FOIS à la création de la réalisation, jamais recalculé :
|
||||
* - Utilisateur humain : REA_NomAffiche = $_SESSION['USR_Prenom'], lui-même
|
||||
* posé une seule fois à la connexion (voir aspic.php) — aucune requête
|
||||
* supplémentaire ici, juste une lecture de session.
|
||||
* - Agent IA : REA_NomAffiche = le nom déjà en clair (CNS_Agent, voir
|
||||
* moteur_consignes.php et journaliserRealisationsIA() dans contacts.php) —
|
||||
* "en dur", aucune recherche à faire, la valeur existe déjà.
|
||||
* Nullable : une ligne plus ancienne, créée avant l'ajout de cette colonne,
|
||||
* n'en a pas — le front-end retombe alors sur REA_CodeUser tel quel (voir
|
||||
* liste_contacts.html, même principe que ACT_Libelle/REA_Action juste à
|
||||
* côté).
|
||||
*/
|
||||
|
||||
session_start();
|
||||
@@ -50,9 +71,12 @@ try {
|
||||
echo json_encode(['error' => 'id (REA_IncrContact) manquant']);
|
||||
exit;
|
||||
}
|
||||
// REA_NomAffiche lu directement (voir commentaire d'en-tête) :
|
||||
// aucune jointure nécessaire, la colonne porte déjà la bonne
|
||||
// valeur depuis la création de la réalisation.
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT r.REA_Incr, r.REA_Date, r.REA_CodeUser, r.REA_Action, r.REA_Comment,
|
||||
a.ACT_Libelle
|
||||
'SELECT r.REA_Incr, r.REA_Date, r.REA_CodeUser, r.REA_NomAffiche,
|
||||
r.REA_Action, r.REA_Comment, a.ACT_Libelle
|
||||
FROM ASP_Realisations r
|
||||
LEFT JOIN ASP_Actions a ON a.ACT_Code = r.REA_Action
|
||||
WHERE r.REA_IncrContact = ?
|
||||
@@ -72,17 +96,21 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// REA_CodeUser vient de la session (voir en-tête de ce fichier),
|
||||
// plus du corps de la requête : impossible pour le client de se
|
||||
// faire passer pour un autre utilisateur.
|
||||
// REA_CodeUser ET REA_NomAffiche viennent tous les deux de la
|
||||
// session (voir en-tête de ce fichier) — jamais du corps de la
|
||||
// requête, impossible pour le client de se faire passer pour un
|
||||
// autre utilisateur. REA_NomAffiche = USR_Prenom posé une seule
|
||||
// fois à la connexion (aspic.php) : aucune requête supplémentaire
|
||||
// ici.
|
||||
$codeUser = $_SESSION['USR_Code'];
|
||||
$nomAffiche = $_SESSION['USR_Prenom'] ?? null;
|
||||
$commentaire = $data['REA_Comment'] ?? '';
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_Date, REA_Action, REA_Comment)
|
||||
VALUES (?, ?, NOW(), ?, ?)'
|
||||
'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_NomAffiche, REA_Date, REA_Action, REA_Comment)
|
||||
VALUES (?, ?, ?, NOW(), ?, ?)'
|
||||
);
|
||||
$stmt->execute([$idContact, $codeUser, $action, $commentaire]);
|
||||
$stmt->execute([$idContact, $codeUser, $nomAffiche, $action, $commentaire]);
|
||||
echo json_encode(['id' => (int)$pdo->lastInsertId()]);
|
||||
break;
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
* capacité à détecter un déploiement resté en retard.
|
||||
*/
|
||||
|
||||
const ASPIC_VERSION = 'V1.14.15';
|
||||
const ASPIC_VERSION = 'V1.15.17';
|
||||
|
||||
if (basename($_SERVER['SCRIPT_NAME']) === 'version.php') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
Reference in New Issue
Block a user