feat(ansible): Ajoute la mise à jour du journal client et permissions fichier

 feat(HomeController): Récupère et affiche le journal de bord client.

📦️  chore: Ajoute gemini-api-php/client et corrige des dépendances.

🐛 fix(docker): Supprime la configuration Xdebug obsolète.
```
This commit is contained in:
Serreau Jovann
2026-01-27 23:10:54 +01:00
parent 80803e2662
commit 63ee6b71c6
8 changed files with 276 additions and 52 deletions

View File

@@ -203,7 +203,22 @@
args:
chdir: "{{ path }}"
when: ansible_os_family == "Debian"
- name: Exécuter app:git-log-update pour mettre à jour le journal client
ansible.builtin.command: php bin/console app:git-log-update
become: false
args:
chdir: "{{ path }}"
# On ignore les erreurs pour ne pas bloquer le déploiement si l'IA ou Git échoue
ignore_errors: yes
- name: S'assurer que le fichier update.json a les bonnes permissions
ansible.builtin.file:
path: "{{ path }}/var/update.json"
owner: bot
group: www-data
mode: '0664'
state: file
ignore_errors: yes
- name: Exécuter liip:imagine:cache:remove dans le répertoire de l application
ansible.builtin.command: php bin/console liip:imagine:cache:remove
become: false

View File

@@ -21,6 +21,7 @@
"exbil/mailcow-php-api": ">=0.15.0",
"fkrzski/robots-txt": "^2.0",
"fpdf/fpdf": "^1.86",
"gemini-api-php/client": "^1.7",
"google/apiclient": "^2.19.0",
"google/cloud": "^0.296.0",
"healey/robots": "^1.0.1",

69
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d69b2f764686c771554adde51917a972",
"content-hash": "04441998f46efc5154a83945a7ca9a46",
"packages": [
{
"name": "async-aws/core",
@@ -2536,6 +2536,73 @@
},
"time": "2025-12-08T14:03:59+00:00"
},
{
"name": "gemini-api-php/client",
"version": "v1.7.2",
"source": {
"type": "git",
"url": "https://github.com/gemini-api-php/client.git",
"reference": "a48e61285d82b24117a5c8928dd1e504818f908b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gemini-api-php/client/zipball/a48e61285d82b24117a5c8928dd1e504818f908b",
"reference": "a48e61285d82b24117a5c8928dd1e504818f908b",
"shasum": ""
},
"require": {
"php": "^8.1",
"php-http/discovery": "^1.19",
"psr/http-client": "^1.0",
"psr/http-client-implementation": "*",
"psr/http-factory": "^1.0.2",
"psr/http-factory-implementation": "*",
"psr/http-message": "^1.0.1 || ^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.41",
"guzzlehttp/guzzle": "^7.8.0",
"guzzlehttp/psr7": "^2.0.0",
"phpstan/phpstan": "^1.10.50",
"phpunit/phpunit": "^10.5"
},
"suggest": {
"ext-curl": "Required for streaming responses"
},
"type": "library",
"autoload": {
"psr-4": {
"GeminiAPI\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Erdem Köse",
"email": "erdemkose@gmail.com"
}
],
"description": "API client for Google's Gemini API",
"keywords": [
"Gemini",
"ai",
"api",
"client",
"gemini pro",
"gemini pro vision",
"google",
"php",
"sdk"
],
"support": {
"issues": "https://github.com/gemini-api-php/client/issues",
"source": "https://github.com/gemini-api-php/client/tree/v1.7.2"
},
"time": "2025-01-31T15:31:02+00:00"
},
{
"name": "google/apiclient",
"version": "v2.19.0",

View File

@@ -54,25 +54,6 @@ RUN pecl install redis && docker-php-ext-enable redis
# Installer Excimer via pecl
RUN pecl install excimer && docker-php-ext-enable excimer
# Configuration et installation de Xdebug
# Utilisation de --no-install-recommends pour éviter l'installation de paquets inutiles
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& mkdir -p /usr/src/php/ext/xdebug \
&& git clone https://github.com/xdebug/xdebug.git /usr/src/php/ext/xdebug \
&& cd /usr/src/php/ext/xdebug \
&& phpize \
&& ./configure --enable-xdebug \
&& make && make install \
&& rm -rf /usr/src/php/ext/xdebug \
&& apt-get autoremove -y build-essential git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY ./docker/php/custom.ini /usr/local/etc/php/conf.d/custom.ini
RUN echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.mode=develop,debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN mkdir -p /opt/phpstorm-coverage && \
chmod -R 777 /opt/phpstorm-coverage

View File

@@ -0,0 +1,124 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Process;
// Utilisation des namespaces que tu as fournis
use GeminiAPI\Client;
use GeminiAPI\Resources\Parts\TextPart;
#[AsCommand(
name: 'app:git-log-update',
description: 'Archive le dernier commit avec reformulation IA pour le client.',
)]
class GitSyncLogCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filePath = 'var/update.json';
$projectDir = '/srv/app';
// 1. Récupération des infos Git
$gitCmd = sprintf(
'git config --global --add safe.directory %s && git log -1 --format="%%s|%%ci|%%h"',
$projectDir
);
$process = Process::fromShellCommandline($gitCmd);
$process->run();
if (!$process->isSuccessful()) {
$io->error("Erreur Git : " . $process->getErrorOutput());
return Command::FAILURE;
}
$outputGit = explode('|', trim($process->getOutput()));
$rawMessage = $outputGit[0] ?? '';
$commitDate = $outputGit[1] ?? date('Y-m-d H:i:s');
$commitHash = $outputGit[2] ?? 'unknown';
// 2. Détermination du TYPE (feature, fix, optimise, new)
$type = 'new';
$lowerMsg = strtolower($rawMessage);
if (preg_match('/(fix|bug|patch|correct)/', $lowerMsg)) {
$type = 'fix';
} elseif (preg_match('/(feat|add|create|nouveau|new)/', $lowerMsg)) {
$type = 'feature';
} elseif (preg_match('/(perf|opti|refactor|clean|speed)/', $lowerMsg)) {
$type = 'optimise';
}
// 3. Vérification anti-doublon (Basée sur le Hash)
$data = [];
if (file_exists($filePath)) {
$data = json_decode(file_get_contents($filePath), true) ?? [];
}
if (!empty($data) && $data[0]['hash'] === $commitHash) {
$io->info("Le commit [$commitHash] est déjà dans le journal client.");
return Command::SUCCESS;
}
// 4. Appel IA Gemini-3-Pro-Preview pour la reformulation
$friendlyMessage = $rawMessage;
try {
// Ta clé API
$client = new Client("AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg");
$model = 'gemini-3-pro-preview';
$prompt = "Tu es un expert en communication web pour Ludik Event. Ta mission est de transformer
un message de commit technique en une note de mise à jour élégante pour ton client.
MESSAGE TECHNIQUE : \"$rawMessage\"
DIRECTIVES :
1. Reformule pour un propriétaire de site non-technique.
2. Sois court, positif et rassurant.
3. Ne commence JAMAIS par 'Voici la phrase' ou 'Mise à jour'.
4. Donne uniquement le texte final prêt à être affiché.
RÉSULTAT ATTENDU :";
$response = $client->withV1BetaVersion()->generativeModel($model)->generateContent(
new TextPart($prompt)
);
// Adaptation selon la structure de retour du SDK
$aiText = $response->text();
if ($aiText) {
$friendlyMessage = trim($aiText);
}
} catch (\Exception $e) {
$io->warning("L'IA n'a pas pu traiter le message. Utilisation du texte brut.");
}
// 5. Création de l'entrée JSON
$newEntry = [
'type' => $type,
'message' => $friendlyMessage,
'date' => $commitDate,
'hash' => $commitHash
];
// 6. Sauvegarde et rotation (5 max)
array_unshift($data, $newEntry);
$data = array_slice($data, 0, 5);
if (!is_dir('var')) {
mkdir('var', 0777, true);
}
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$io->success("Journal client mis à jour avec succès (Type: $type).");
return Command::SUCCESS;
}
}

View File

@@ -29,53 +29,65 @@ class HomeController extends AbstractController
// 1. Récupération sécurisée du Token (Cache 2h)
$token = $cache->get('umami_token', function (ItemInterface $item) use ($httpClient, $baseUrl) {
$item->expiresAfter(7200);
$response = $httpClient->request('POST', "$baseUrl/auth/login", [
'json' => [
'username' => $_ENV['UMAMI_USER'],
'password' => $_ENV['UMAMI_PASSWORD'],
]
]);
return $response->toArray()['token'] ?? null;
});
// 2. Récupération des Stats (Cache 15 min pour la fluidité du CRM)
// 2. Récupération des Stats Umami
$stats = $cache->get('umami_stats_24h', function (ItemInterface $item) use ($httpClient, $baseUrl, $token, $websiteId) {
$item->expiresAfter(900); // 15 minutes
$item->expiresAfter(900);
if (!$token) return ['visitors' => 0, 'views' => 0];
try {
$startAt = (time() - (24 * 3600)) * 1000;
$endAt = time() * 1000;
$response = $httpClient->request('GET', "$baseUrl/websites/$websiteId/stats", [
'headers' => ['Authorization' => "Bearer $token"],
'query' => [
'startAt' => $startAt,
'endAt' => $endAt,
],
'query' => ['startAt' => $startAt, 'endAt' => $endAt],
]);
$data = $response->toArray();
return [
'visitors' => $data['visitors'] ?? 0,
'views' => $data['pageviews'] ?? 0
'visitors' => $data['visitors']['value'] ?? $data['visitors'] ?? 0,
'views' => $data['pageviews']['value'] ?? $data['pageviews'] ?? 0
];
} catch (\Exception $e) {
return ['visitors' => 0, 'views' => 0];
}
});
// 3. Récupération des Updates (Journal de bord client)
$updateFile = $this->getParameter('kernel.project_dir') . '/var/update.json';
$updates = [];
if (file_exists($updateFile)) {
$rawUpdates = json_decode(file_get_contents($updateFile), true) ?? [];
// On enrichit les données avec les couleurs Tailwind
$updates = array_map(function ($update) {
$update['tag_color'] = match ($update['type'] ?? 'new') {
'feature' => 'bg-emerald-100 text-emerald-700 border-emerald-200',
'fix' => 'bg-rose-100 text-rose-700 border-rose-200',
'optimise' => 'bg-amber-100 text-amber-700 border-amber-200',
'new' => 'bg-slate-100 text-slate-700 border-slate-200',
default => 'bg-gray-100 text-gray-700 border-gray-200',
};
return $update;
}, $rawUpdates);
}
return $this->render('dashboard/home.twig', [
'product' => $productRepository->count(),
'devis_wait_sign' => $devisRepository->waitSign(),
'customers' => $customerRepository->count(),
'nbVisitor' => $stats['visitors'],
'nbView' => $stats['views'],
'statview' => "https://tools-security.esy-web.dev/share/o9j2XMjV4Trnnbfb"
'statview' => "https://tools-security.esy-web.dev/share/o9j2XMjV4Trnnbfb",
'updates' => $updates // Passage des mises à jour au template
]);
}
}

View File

@@ -16,7 +16,6 @@
<div class="flex h-screen overflow-hidden">
{# SIDEBAR #}
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
@@ -216,7 +215,6 @@
</div>
</div>
</main> {# Fin du main existant #}
</div>
</body>
</html>

View File

@@ -8,7 +8,6 @@
{# --- HEADER STATS (GLASS PILLS) --- #}
<div class="flex flex-wrap gap-4 items-center mb-8">
<div class="flex flex-col md:flex-row md:items-center gap-4 w-full md:w-auto">
{# Titre / Lien vers le site #}
<div class="flex items-center gap-2 px-2">
<h1 class="text-xl font-black italic text-slate-900 dark:text-white uppercase tracking-tighter">
Analytics
@@ -24,7 +23,6 @@
</div>
<div class="flex gap-3">
{# Badge Visiteurs #}
<div class="backdrop-blur-md bg-white/40 dark:bg-slate-900/20 border border-white/40 dark:border-white/10 px-6 py-2.5 rounded-full shadow-lg flex items-center gap-3">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span class="text-[11px] font-black uppercase tracking-widest text-slate-600 dark:text-slate-300">
@@ -32,7 +30,6 @@
</span>
</div>
{# Badge Vues #}
<div class="backdrop-blur-md bg-white/40 dark:bg-slate-900/20 border border-white/40 dark:border-white/10 px-6 py-2.5 rounded-full shadow-lg flex items-center gap-3">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
@@ -82,15 +79,44 @@
</div>
</div>
{# Carte Clients #}
<div class="group relative">
{# Carte Journal de Maintenance #}
<div class="group relative md:row-span-1">
<div class="absolute inset-0 bg-gradient-to-br from-purple-600/20 to-transparent blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative h-full overflow-hidden backdrop-blur-2xl bg-white/30 dark:bg-slate-800/40 border border-white/20 dark:border-white/5 rounded-[3rem] p-8 shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2">
<p class="text-[10px] font-black text-purple-600 dark:text-purple-400 uppercase tracking-[0.4em] mb-6 italic">Maintenance</p>
<div class="space-y-6">
{% for update in updates %}
<div class="relative pl-6 border-l-2 border-slate-200 dark:border-slate-700 pb-1">
{# Point sur la timeline #}
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full border-4 border-white dark:border-slate-800 {{ update.tag_color|replace({'text-': 'bg-'})|split(' ')[0] }}"></div>
<div class="flex items-center gap-2 mb-1">
<span class="px-2 py-0.5 rounded-md border text-[8px] font-black uppercase tracking-tighter {{ update.tag_color }}">
{{ update.type }}
</span>
<span class="text-[9px] font-bold text-slate-400 uppercase">{{ update.date|date('d/m/y') }}</span>
</div>
<p class="text-[11px] font-semibold text-slate-700 dark:text-slate-200 leading-tight">
{{ update.message }}
</p>
</div>
{% else %}
<p class="text-xs text-slate-400 italic">Aucune mise à jour disponible.</p>
{% endfor %}
</div>
</div>
</div>
{# Carte Clients (Déplacée en bas ou sur le côté selon ton envie de grille) #}
<div class="group relative md:col-span-2">
<div class="absolute inset-0 bg-gradient-to-br from-emerald-600/20 to-transparent blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative overflow-hidden backdrop-blur-2xl bg-white/30 dark:bg-slate-800/40 border border-white/20 dark:border-white/5 rounded-[3rem] p-8 shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2">
<div class="flex items-start justify-between">
<div class="flex items-center justify-between">
<div>
<p class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 uppercase tracking-[0.4em] mb-4 italic">BDD Clients</p>
<p class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 uppercase tracking-[0.4em] mb-4 italic">Client(s)</p>
<h2 class="text-7xl font-black text-slate-900 dark:text-white tracking-tighter italic leading-none">{{ customers }}</h2>
<p class="text-[11px] font-bold text-slate-500 dark:text-slate-400 mt-4 uppercase tracking-wider">Fiches clients uniques</p>
<p class="text-[11px] font-bold text-slate-500 dark:text-slate-400 mt-4 uppercase tracking-wider">Fiches clients uniques dans la base</p>
</div>
<div class="w-14 h-14 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-2xl flex items-center justify-center text-white shadow-[0_10px_20px_rgba(16,185,129,0.3)]">
<svg class="w-7 h-7" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>