# Planification - icone barre des taches (best-effort, optionnel) // 2026.1.4 - QRO # Developpe par Quentin Rouiller (QRO) - DGNSI, Canton de Vaud. # Licence MIT - voir le fichier LICENSE. # # Indique que le helper de sync est installe. Clic / menu -> fenetre du journal # (aspect application : journal colore par niveau, auto-rafraichi, bouton # "Tout afficher"). Lance par tray.vbs (fenetre masquee), au login. $ErrorActionPreference = 'SilentlyContinue' # Instance unique. $mutex = New-Object System.Threading.Mutex($false, 'Local\PlanificationTraySingleton') if (-not $mutex.WaitOne(0)) { exit } Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # tray.ps1 vit dans \bin -> racine = parent ; assets et log a la racine. $root = if ($PSScriptRoot) { Split-Path -Parent $PSScriptRoot } else { Join-Path $env:LOCALAPPDATA 'Planification' } $logFile = Join-Path $root 'activity.log' $iconPng = Join-Path (Join-Path $root 'assets') 'icon.png' function Get-TrayIcon { try { if (Test-Path -LiteralPath $iconPng) { $bmp = New-Object System.Drawing.Bitmap($iconPng) return [System.Drawing.Icon]::FromHandle($bmp.GetHicon()) } } catch {} return [System.Drawing.SystemIcons]::Information } $script:logForm = $null $script:logRtb = $null $script:logChk = $null $script:logSig = '' $colInfo = [System.Drawing.Color]::FromArgb(40, 70, 120) $colDebug = [System.Drawing.Color]::FromArgb(150, 150, 150) $colErr = [System.Drawing.Color]::FromArgb(200, 40, 40) $colTs = [System.Drawing.Color]::FromArgb(120, 120, 120) function Update-LogText { if (-not $script:logRtb -or $script:logRtb.IsDisposed) { return } $lines = @() if (Test-Path -LiteralPath $logFile) { try { $all = Get-Content -LiteralPath $logFile -Encoding UTF8 -ErrorAction SilentlyContinue if ($all) { if ($script:logChk -and -not $script:logChk.Checked) { $all = $all | Where-Object { $_ -notmatch '\[DEBUG\]' } } $lines = @($all | Select-Object -Last 500) } } catch {} } $sig = ($lines -join "`n") if ($sig -eq $script:logSig) { return } $script:logSig = $sig $rtb = $script:logRtb $rtb.SuspendLayout() $rtb.Clear() if ($lines.Count -eq 0) { $rtb.SelectionColor = $colDebug $rtb.AppendText("(aucune activite de synchronisation pour le moment)") } else { foreach ($line in $lines) { $col = $colInfo if ($line -match '\[ERREUR\]') { $col = $colErr } elseif ($line -match '\[DEBUG\]') { $col = $colDebug } if ($line -match '^(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2})(.*)$') { $rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $colTs $rtb.AppendText($Matches[1]) $rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col $rtb.AppendText($Matches[2] + "`n") } else { $rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col $rtb.AppendText($line + "`n") } } } $rtb.ResumeLayout() $rtb.SelectionStart = $rtb.TextLength; $rtb.ScrollToCaret() } function Show-LogWindow { if ($script:logForm -and -not $script:logForm.IsDisposed) { $script:logForm.WindowState = 'Normal'; $script:logForm.Activate(); return } $form = New-Object System.Windows.Forms.Form $form.Text = 'Planification - journal de synchronisation' $form.Size = New-Object System.Drawing.Size(760, 540) $form.StartPosition = 'CenterScreen' $form.BackColor = [System.Drawing.Color]::FromArgb(245, 246, 248) try { $form.Icon = Get-TrayIcon } catch {} # 1) zone de texte (Fill) ajoutee en premier $rtb = New-Object System.Windows.Forms.RichTextBox $rtb.Dock = 'Fill' $rtb.ReadOnly = $true $rtb.BorderStyle = 'None' $rtb.BackColor = [System.Drawing.Color]::White $rtb.Font = New-Object System.Drawing.Font('Consolas', 9.5) $rtb.DetectUrls = $false $form.Controls.Add($rtb) # 2) barre d'outils claire avec le BOUTON bascule "Tout afficher" bien visible $bar = New-Object System.Windows.Forms.Panel $bar.Dock = 'Top'; $bar.Height = 40 $bar.BackColor = [System.Drawing.Color]::FromArgb(238, 240, 243) $chk = New-Object System.Windows.Forms.CheckBox $chk.Appearance = 'Button' $chk.Text = 'Tout afficher (mode detaille)' $chk.TextAlign = 'MiddleCenter' $chk.AutoSize = $false $chk.Size = New-Object System.Drawing.Size(200, 28) $chk.Location = New-Object System.Drawing.Point(10, 6) $chk.FlatStyle = 'System' $chk.add_CheckedChanged({ $script:logSig = '~'; Update-LogText }) $bar.Controls.Add($chk) $hint = New-Object System.Windows.Forms.Label $hint.Text = 'Par defaut : l''essentiel. Activez pour voir tout le detail technique.' $hint.ForeColor = [System.Drawing.Color]::FromArgb(90, 90, 90) $hint.AutoSize = $true $hint.Location = New-Object System.Drawing.Point(220, 12) $bar.Controls.Add($hint) $form.Controls.Add($bar) # 3) bandeau titre (bleu) ajoute en dernier -> tout en haut $hdrBar = New-Object System.Windows.Forms.Panel $hdrBar.Dock = 'Top'; $hdrBar.Height = 32 $hdrBar.BackColor = [System.Drawing.Color]::FromArgb(40, 70, 120) $hdr = New-Object System.Windows.Forms.Label $hdr.Text = 'Activite de synchronisation - Planification' $hdr.ForeColor = [System.Drawing.Color]::White $hdr.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold) $hdr.AutoSize = $true $hdr.Location = New-Object System.Drawing.Point(10, 7) $hdrBar.Controls.Add($hdr) $form.Controls.Add($hdrBar) $script:logForm = $form $script:logRtb = $rtb $script:logChk = $chk $script:logSig = '' Update-LogText $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 2000 $timer.add_Tick({ Update-LogText }) $timer.Start() $form.add_FormClosed({ $timer.Stop(); $timer.Dispose() }) $form.Show(); $form.Activate() } $ni = New-Object System.Windows.Forms.NotifyIcon $ni.Icon = Get-TrayIcon $ni.Text = 'Planification - synchronisation active' $ni.Visible = $true # v2026.1.8 - "toujours present si actif" : le NotifyIcon disparait quand # explorer.exe redemarre (Windows rediffuse alors le message "TaskbarCreated"). # Sans le traiter, l'icone ne revient JAMAIS tant que le tray tourne. On ecoute # ce message via une petite fenetre native et on re-affiche l'icone. try { Add-Type -ReferencedAssemblies System.Windows.Forms -TypeDefinition @' using System; using System.Windows.Forms; using System.Runtime.InteropServices; public class PlanifTrayWatcher : NativeWindow { [DllImport("user32.dll", CharSet = CharSet.Auto)] static extern uint RegisterWindowMessage(string lpString); public static readonly uint TaskbarCreated = RegisterWindowMessage("TaskbarCreated"); private NotifyIcon _icon; public PlanifTrayWatcher(NotifyIcon icon) { _icon = icon; this.CreateHandle(new CreateParams()); } protected override void WndProc(ref Message m) { if (m.Msg == TaskbarCreated && _icon != null) { try { _icon.Visible = false; _icon.Visible = true; } catch {} } base.WndProc(ref m); } } '@ -ErrorAction Stop $script:trayWatcher = New-Object PlanifTrayWatcher($ni) } catch {} $menu = New-Object System.Windows.Forms.ContextMenuStrip $miLog = $menu.Items.Add('Voir le journal de synchronisation') $miLog.add_Click({ Show-LogWindow }) $miQuit = $menu.Items.Add('Quitter') $miQuit.add_Click({ $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() }) $ni.ContextMenuStrip = $menu $ni.add_MouseClick({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) { Show-LogWindow } }) # v2026.1.7 - auto-extinction apres 2 h SANS activite de synchronisation. On se # base sur la date de derniere ecriture du journal (le host y ecrit a chaque # operation). Le navigateur relancera le host a la prochaine activite, qui # relancera ce tray. -> "de base eteint, s'active a l'usage, se ferme au repos". $IDLE_LIMIT_MIN = 120 $idleTimer = New-Object System.Windows.Forms.Timer $idleTimer.Interval = 300000 # verifie toutes les 5 min $idleTimer.add_Tick({ try { $age = $null if (Test-Path -LiteralPath $logFile) { $age = ((Get-Date) - (Get-Item -LiteralPath $logFile).LastWriteTime).TotalMinutes } # Pas de journal, ou journal inactif depuis >= 2 h -> on s'eteint. if (($null -eq $age) -or ($age -ge $IDLE_LIMIT_MIN)) { $ni.Visible = $false; $ni.Dispose() [System.Windows.Forms.Application]::Exit() } } catch {} }) $idleTimer.Start() [System.Windows.Forms.Application]::EnableVisualStyles() [System.Windows.Forms.Application]::Run() $idleTimer.Stop() $ni.Dispose()