diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml
index dbca559..dfd8bef 100644
--- a/ansible/inventory/hosts.yml
+++ b/ansible/inventory/hosts.yml
@@ -13,7 +13,10 @@ all:
env_lab:
hosts:
dev.lab.home: null
+ media-1.lab.home: null
media.labb.home: null
+ openclaw.lab.home:
+ ansible_host: 192.168.30.111
env_prod:
hosts:
ali2v.truenas.home: null
@@ -36,7 +39,9 @@ all:
dev.lab.home: null
dev.prod.home: null
jump.point.home: null
+ media-1.lab.home: null
media.labb.home: null
+ openclaw.lab.home: null
orangepi.pc.home: null
raspi.4gb.home: null
raspi.8gb.home: null
diff --git a/app/crud/terminal_command_log.py b/app/crud/terminal_command_log.py
index 15e500f..5f65b85 100644
--- a/app/crud/terminal_command_log.py
+++ b/app/crud/terminal_command_log.py
@@ -139,6 +139,8 @@ class TerminalCommandLogRepository:
async def list_global(
self,
query: Optional[str] = None,
+ time_filter: str = "all",
+ pinned_only: bool = False,
host_id: Optional[str] = None,
user_id: Optional[str] = None,
limit: int = 50,
@@ -146,13 +148,6 @@ class TerminalCommandLogRepository:
) -> List[TerminalCommandLog]:
"""
List command logs globally with optional filters.
-
- Args:
- query: Optional search query
- host_id: Optional host ID to filter by
- user_id: Optional user ID to filter by
- limit: Maximum number of results
- offset: Number of results to skip
"""
conditions = [TerminalCommandLog.is_blocked == False]
@@ -164,6 +159,23 @@ class TerminalCommandLogRepository:
if query:
conditions.append(TerminalCommandLog.command.ilike(f"%{query}%"))
+
+ if pinned_only:
+ conditions.append(TerminalCommandLog.is_pinned == True)
+
+ if time_filter != "all":
+ now = datetime.now(timezone.utc)
+ if time_filter == "today":
+ cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
+ elif time_filter == "week":
+ cutoff = now - timedelta(days=7)
+ elif time_filter == "month":
+ cutoff = now - timedelta(days=30)
+ else:
+ cutoff = None
+
+ if cutoff:
+ conditions.append(TerminalCommandLog.created_at >= cutoff)
stmt = (
select(TerminalCommandLog)
diff --git a/app/index.html b/app/index.html
index d52a5dc..3a9d511 100644
--- a/app/index.html
+++ b/app/index.html
@@ -3167,20 +3167,25 @@
/* Terminal History Panel */
.terminal-history-panel {
+ position: absolute;
+ top: 57px; /* Matches header height */
+ left: 0;
+ right: 0;
+ z-index: 100;
background: #1e1e2e;
border-bottom: 1px solid #374151;
- max-height: 350px;
+ max-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
opacity: 0;
- transform: translateY(-10px);
- transition: opacity 0.2s ease, transform 0.2s ease;
+ transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease;
+ box-shadow: 0 5px 25px rgba(0,0,0,0.5);
}
.terminal-history-panel.open {
+ max-height: 350px;
opacity: 1;
- transform: translateY(0);
}
.terminal-history-header {
@@ -3308,7 +3313,11 @@
/* Docked mode: panel stays visible and doesn't close on command execute */
.terminal-history-panel.docked {
+ position: relative;
+ top: 0;
max-height: 280px;
+ z-index: 1;
+ box-shadow: none;
border-bottom: 2px solid #7c3aed;
}
@@ -3395,6 +3404,17 @@
color: #6b7280;
cursor: help;
}
+
+ .terminal-history-host {
+ font-size: 0.6875rem;
+ color: #4ade80;
+ background: rgba(34, 197, 94, 0.1);
+ padding: 0.125rem 0.375rem;
+ border-radius: 4px;
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ margin-left: auto;
+ white-space: nowrap;
+ }
.terminal-history-count {
font-size: 0.625rem;
diff --git a/app/main.js b/app/main.js
index eb410da..79e032b 100644
--- a/app/main.js
+++ b/app/main.js
@@ -11726,6 +11726,8 @@ class DashboardManager {
showTerminalDrawer(hostName, hostIp, loading = false) {
this.terminalDrawerOpen = true;
+ this.terminalHistoryPanelOpen = false;
+ this.terminalHistoryPanelPinned = false;
// Create drawer if it doesn't exist
let drawer = document.getElementById('terminalDrawer');
@@ -11963,6 +11965,8 @@ class DashboardManager {
}
this.terminalDrawerOpen = false;
+ this.terminalHistoryPanelOpen = false;
+ this.terminalHistoryPanelPinned = false;
}
// ===== TERMINAL CLEANUP HANDLERS =====
@@ -12260,7 +12264,7 @@ class DashboardManager {
async toggleTerminalHistory() {
if (this.terminalHistoryPanelOpen) {
- this.closeTerminalHistoryPanel();
+ this.closeTerminalHistoryPanel(true);
} else {
this.openTerminalHistoryPanel();
}
@@ -12292,9 +12296,9 @@ class DashboardManager {
}
}
- closeTerminalHistoryPanel() {
- // If panel is pinned/docked, don't close on command execute
- if (this.terminalHistoryPanelPinned) return;
+ closeTerminalHistoryPanel(force = false) {
+ // If panel is pinned/docked, don't close on command execute unless forced
+ if (this.terminalHistoryPanelPinned && !force) return;
const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('terminalHistoryBtn');
@@ -12577,8 +12581,8 @@ class DashboardManager {
const newPinned = !cmd.is_pinned;
try {
- const hostId = this.terminalSession.host.id;
- await this.apiCall(`/api/terminal/${hostId}/command-history/${cmd.command_hash}/pin`, {
+ const hId = cmd.host_id || this.terminalSession.host.id;
+ await this.apiCall(`/api/terminal/${hId}/command-history/${cmd.command_hash}/pin`, {
method: 'POST',
body: JSON.stringify({ is_pinned: newPinned })
});
@@ -12620,7 +12624,7 @@ class DashboardManager {
if (found) {
found.term.focus();
found.term.paste(command);
- // Don't close panel (insert mode)
+ this.closeTerminalHistoryPanel();
return;
}
diff --git a/app/routes/terminal.py b/app/routes/terminal.py
index cf7da97..82d71e8 100644
--- a/app/routes/terminal.py
+++ b/app/routes/terminal.py
@@ -202,9 +202,8 @@ async def _verify_history_access(
if current_user:
return True
- token = _get_session_token_from_request(request, host_id, token=request.query_params.get("token"))
- if not token:
- return False
+ # Try query param first (generic)
+ q_token = request.query_params.get("token")
session_repo = TerminalSessionRepository(db_session)
# Try by ID first
@@ -218,7 +217,9 @@ async def _verify_history_access(
sessions = await session_repo.list_active_for_host(host.id)
for s in sessions:
- if terminal_service.verify_token(token, s.token_hash):
+ # Check if token is in query param OR in the session-specific cookie
+ token = q_token or _get_session_token_from_request(request, s.id)
+ if token and terminal_service.verify_token(token, s.token_hash):
return True
return False
@@ -1111,7 +1112,7 @@ async def get_terminal_connect_page(
// ---- Panel open/close ----
function toggleHistory() {
- if (historyPanelOpen && !historyPanelPinned) { closeHistoryPanel(); return; }
+ if (historyPanelOpen) { closeHistoryPanel(true); return; }
const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('btnHistory');
panel.style.display = 'flex'; panel.classList.add('open'); btn.classList.add('active');
@@ -1120,8 +1121,8 @@ async def get_terminal_connect_page(
if (si) { si.focus(); si.select(); }
if (historyData.length === 0) loadHistory();
}
- function closeHistoryPanel() {
- if (historyPanelPinned) return;
+ function closeHistoryPanel(force = false) {
+ if (historyPanelPinned && !force) return;
const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('btnHistory');
panel.classList.remove('open'); btn.classList.remove('active');
@@ -1174,8 +1175,9 @@ async def get_terminal_connect_page(
const cmd = historyData[i];
if (!cmd || !cmd.command_hash) return;
const newPinned = !cmd.is_pinned;
+ const hId = cmd.host_id || HOST_ID;
try {
- const res = await fetch(`/api/terminal/${HOST_ID}/command-history/${cmd.command_hash}/pin?token=${encodeURIComponent(TOKEN)}`, {
+ const res = await fetch(`/api/terminal/${hId}/command-history/${cmd.command_hash}/pin?token=${encodeURIComponent(TOKEN)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
body: JSON.stringify({ is_pinned: newPinned })
@@ -1214,10 +1216,10 @@ async def get_terminal_connect_page(
const list=document.getElementById('terminalHistoryList'),q=historySearchQuery||'';
if(!historyData.length){list.innerHTML=`
${q?`Aucun résultat pour "${escH(q)}"`:historyPinnedOnly?'Aucune commande épinglée':'Aucune commande'}
`;return;}
list.innerHTML=historyData.map((cmd,i)=>{
- const c=cmd.command||'',ta=relTime(cmd.last_used||cmd.created_at),ec=cmd.execution_count||1,sel=i===historySelectedIndex,pinned=cmd.is_pinned;
+ const c=cmd.command||'',ta=relTime(cmd.last_used||cmd.created_at),ec=cmd.execution_count||1,sel=i===historySelectedIndex,pinned=cmd.is_pinned,hn=cmd.host_name||'';
let dc=escH(c.length>80?c.substring(0,80)+'...':c);
if(q)dc=dc.replace(new RegExp(`(${escRE(q)})`,'gi'),'$1');
- return `${pinned?'':''}${dc}
${ta}${ec>1?`×${ec}`:''}
`;
+ return `${pinned?'':''}${dc}
${ta}${ec>1?`×${ec}`:''}${hn?`${escH(hn)}`:''}
`;
}).join('');
if(historySelectedIndex>=0){const s=list.querySelector('.selected');if(s)s.scrollIntoView({block:'nearest',behavior:'smooth'});}
}
@@ -1420,9 +1422,12 @@ async def get_host_command_history(
CommandHistoryItem(
id=log.id,
command=log.command,
+ command_hash=log.command_hash,
created_at=log.created_at,
+ host_id=log.host_id,
host_name=log.host_name,
username=log.username,
+ is_pinned=log.is_pinned,
)
for log in logs
]
@@ -1597,6 +1602,8 @@ async def toggle_command_pin(
async def get_global_command_history(
request: Request,
query: Optional[str] = None,
+ time_filter: str = "all",
+ pinned_only: bool = False,
host_id: Optional[str] = None,
limit: int = 50,
offset: int = 0,
@@ -1623,6 +1630,8 @@ async def get_global_command_history(
cmd_repo = TerminalCommandLogRepository(db_session)
logs = await cmd_repo.list_global(
query=query,
+ time_filter=time_filter,
+ pinned_only=pinned_only,
host_id=host_id,
user_id=str(user_id) if user_id else None,
limit=min(limit, 100),
@@ -1633,9 +1642,12 @@ async def get_global_command_history(
CommandHistoryItem(
id=log.id,
command=log.command,
+ command_hash=log.command_hash,
created_at=log.created_at,
+ host_id=log.host_id,
host_name=log.host_name,
username=log.username,
+ is_pinned=log.is_pinned,
)
for log in logs
]
diff --git a/app/schemas/terminal.py b/app/schemas/terminal.py
index 6a07612..c7178a1 100644
--- a/app/schemas/terminal.py
+++ b/app/schemas/terminal.py
@@ -15,7 +15,9 @@ class CommandHistoryItem(BaseModel):
"""A single command from history."""
id: int
command: str
+ command_hash: Optional[str] = None
created_at: datetime
+ host_id: Optional[str] = None
host_name: Optional[str] = None
username: Optional[str] = None
execution_count: Optional[int] = None
diff --git a/app/templates/terminal/connect.html b/app/templates/terminal/connect.html
index 4c9516c..587d0e2 100644
--- a/app/templates/terminal/connect.html
+++ b/app/templates/terminal/connect.html
@@ -191,6 +191,12 @@
}
.terminal-history-meta { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.terminal-history-time { font-size: 0.6875rem; color: #6b7280; }
+ .terminal-history-host {
+ font-size: 0.6875rem; color: #4ade80;
+ background: rgba(34, 197, 94, 0.1); padding: 0.125rem 0.375rem;
+ border-radius: 4px; border: 1px solid rgba(34, 197, 94, 0.2);
+ margin-left: auto; white-space: nowrap;
+ }
.terminal-history-count {
font-size: 0.625rem; color: #7c3aed;
background: rgba(124, 58, 237, 0.2); padding: 0.125rem 0.375rem;
@@ -289,11 +295,6 @@
{% if debug_panel_html %}
{{ debug_panel_html | safe }}
{% endif %}
-
+
{{ script_block | safe }}
diff --git a/logs/tasks_logs/2026/03/03/task_181456_9a2d98_media-1.lab.home_Vérification_de_santé_completed.md b/logs/tasks_logs/2026/03/03/task_181456_9a2d98_media-1.lab.home_Vérification_de_santé_completed.md
new file mode 100644
index 0000000..1ffa512
--- /dev/null
+++ b/logs/tasks_logs/2026/03/03/task_181456_9a2d98_media-1.lab.home_Vérification_de_santé_completed.md
@@ -0,0 +1,58 @@
+# ✅ Vérification de santé
+
+## Informations
+
+| Propriété | Valeur |
+|-----------|--------|
+| **ID** | `11e8f1d9d9744a8d8b4f05f7c3cb9dbc` |
+| **Nom** | Vérification de santé |
+| **Cible** | `media-1.lab.home` |
+| **Statut** | completed |
+| **Type** | Manuel |
+| **Progression** | 100% |
+| **Début** | 2026-03-03T18:14:51.218241+00:00 |
+| **Fin** | 2026-03-03T18:14:56.699977+00:00 |
+| **Durée** | 5.5s |
+
+## Sortie
+
+```
+Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
+
+PLAY [Health check on target host] *********************************************
+
+TASK [Check if host is reachable (ping)] ***************************************
+ok: [media-1.lab.home] => {"changed": false, "ping": "pong"}
+
+TASK [Gather minimal facts] ****************************************************
+ok: [media-1.lab.home]
+
+TASK [Get system uptime] *******************************************************
+ok: [media-1.lab.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.002924", "end": "2026-03-03 13:14:52.271861", "msg": "", "rc": 0, "start": "2026-03-03 13:14:52.268937", "stderr": "", "stderr_lines": [], "stdout": " 13:14:52 up 6 days, 5:52, 0 users, load average: 0.37, 0.36, 0.28", "stdout_lines": [" 13:14:52 up 6 days, 5:52, 0 users, load average: 0.37, 0.36, 0.28"]}
+
+TASK [Get disk usage] **********************************************************
+ok: [media-1.lab.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.003590", "end": "2026-03-03 13:14:52.721041", "msg": "", "rc": 0, "start": "2026-03-03 13:14:52.717451", "stderr": "", "stderr_lines": [], "stdout": "85%", "stdout_lines": ["85%"]}
+
+TASK [Get memory usage (Linux)] ************************************************
+ok: [media-1.lab.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.010986", "end": "2026-03-03 13:14:53.179070", "msg": "", "rc": 0, "start": "2026-03-03 13:14:53.168084", "stderr": "", "stderr_lines": [], "stdout": "9.3%", "stdout_lines": ["9.3%"]}
+
+TASK [Get CPU temperature (ARM/SBC)] *******************************************
+ok: [media-1.lab.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.002847", "end": "2026-03-03 13:14:53.629279", "msg": "", "rc": 0, "start": "2026-03-03 13:14:53.626432", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
+
+TASK [Get CPU load] ************************************************************
+ok: [media-1.lab.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.003757", "end": "2026-03-03 13:14:54.084586", "msg": "", "rc": 0, "start": "2026-03-03 13:14:54.080829", "stderr": "", "stderr_lines": [], "stdout": "0.38", "stdout_lines": ["0.38"]}
+
+TASK [Display health status] ***************************************************
+ok: [media-1.lab.home] => {
+ "msg": "═══════════════════════════════════════\nHost: media-1.lab.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 13:14:52 up 6 days, 5:52, 0 users, load average: 0.37, 0.36, 0.28\nDisk Usage: 85%\nMemory Usage: 9.3%\nCPU Load: 0.38\nCPU Temp: N/A\n═══════════════════════════════════════\n"
+}
+
+PLAY RECAP *********************************************************************
+media-1.lab.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
+
+```
+
+---
+*Généré automatiquement par Homelab Automation Dashboard*
+*Date: 2026-03-03T18:14:56.724153+00:00*
diff --git a/logs/tasks_logs/2026/03/03/task_181706_18cec8_openclaw.lab.home_Vérification_de_santé_failed.md b/logs/tasks_logs/2026/03/03/task_181706_18cec8_openclaw.lab.home_Vérification_de_santé_failed.md
new file mode 100644
index 0000000..502e8c8
--- /dev/null
+++ b/logs/tasks_logs/2026/03/03/task_181706_18cec8_openclaw.lab.home_Vérification_de_santé_failed.md
@@ -0,0 +1,35 @@
+# ❌ Vérification de santé
+
+## Informations
+
+| Propriété | Valeur |
+|-----------|--------|
+| **ID** | `e1cbbcd7f94b4314a2cada7121d88f98` |
+| **Nom** | Vérification de santé |
+| **Cible** | `openclaw.lab.home` |
+| **Statut** | failed |
+| **Type** | Manuel |
+| **Progression** | 100% |
+| **Début** | 2026-03-03T18:17:05.574697+00:00 |
+| **Fin** | 2026-03-03T18:17:06.639749+00:00 |
+| **Durée** | 1.1s |
+
+## Sortie
+
+```
+Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
+
+PLAY [Health check on target host] *********************************************
+
+TASK [Check if host is reachable (ping)] ***************************************
+fatal: [openclaw.lab.home]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Warning: Permanently added '192.168.30.111' (ED25519) to the list of known hosts.\r\nautomation@192.168.30.111: Permission denied (publickey,password).", "unreachable": true}
+
+PLAY RECAP *********************************************************************
+openclaw.lab.home : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
+
+
+```
+
+---
+*Généré automatiquement par Homelab Automation Dashboard*
+*Date: 2026-03-03T18:17:06.656631+00:00*
diff --git a/logs/tasks_logs/2026/03/03/task_181851_25f12c_openclaw.lab.home_Vérification_de_santé_completed.md b/logs/tasks_logs/2026/03/03/task_181851_25f12c_openclaw.lab.home_Vérification_de_santé_completed.md
new file mode 100644
index 0000000..cb441cb
--- /dev/null
+++ b/logs/tasks_logs/2026/03/03/task_181851_25f12c_openclaw.lab.home_Vérification_de_santé_completed.md
@@ -0,0 +1,58 @@
+# ✅ Vérification de santé
+
+## Informations
+
+| Propriété | Valeur |
+|-----------|--------|
+| **ID** | `7a57e331ad3846d583e613c02f09cfa9` |
+| **Nom** | Vérification de santé |
+| **Cible** | `openclaw.lab.home` |
+| **Statut** | completed |
+| **Type** | Manuel |
+| **Progression** | 100% |
+| **Début** | 2026-03-03T18:18:46.347326+00:00 |
+| **Fin** | 2026-03-03T18:18:51.383461+00:00 |
+| **Durée** | 5.0s |
+
+## Sortie
+
+```
+Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
+
+PLAY [Health check on target host] *********************************************
+
+TASK [Check if host is reachable (ping)] ***************************************
+ok: [openclaw.lab.home] => {"changed": false, "ping": "pong"}
+
+TASK [Gather minimal facts] ****************************************************
+ok: [openclaw.lab.home]
+
+TASK [Get system uptime] *******************************************************
+ok: [openclaw.lab.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.005568", "end": "2026-03-03 13:18:49.842547", "msg": "", "rc": 0, "start": "2026-03-03 13:18:49.836979", "stderr": "", "stderr_lines": [], "stdout": " 13:18:49 up 2 days, 13:12, 2 users, load average: 0.05, 0.01, 0.00", "stdout_lines": [" 13:18:49 up 2 days, 13:12, 2 users, load average: 0.05, 0.01, 0.00"]}
+
+TASK [Get disk usage] **********************************************************
+ok: [openclaw.lab.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.005402", "end": "2026-03-03 13:18:50.270457", "msg": "", "rc": 0, "start": "2026-03-03 13:18:50.265055", "stderr": "", "stderr_lines": [], "stdout": "96%", "stdout_lines": ["96%"]}
+
+TASK [Get memory usage (Linux)] ************************************************
+ok: [openclaw.lab.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.006000", "end": "2026-03-03 13:18:50.670294", "msg": "", "rc": 0, "start": "2026-03-03 13:18:50.664294", "stderr": "", "stderr_lines": [], "stdout": "34.0%", "stdout_lines": ["34.0%"]}
+
+TASK [Get CPU temperature (ARM/SBC)] *******************************************
+ok: [openclaw.lab.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.003576", "end": "2026-03-03 13:18:51.050744", "msg": "", "rc": 0, "start": "2026-03-03 13:18:51.047168", "stderr": "", "stderr_lines": [], "stdout": "N/A", "stdout_lines": ["N/A"]}
+
+TASK [Get CPU load] ************************************************************
+ok: [openclaw.lab.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.004421", "end": "2026-03-03 13:18:51.419246", "msg": "", "rc": 0, "start": "2026-03-03 13:18:51.414825", "stderr": "", "stderr_lines": [], "stdout": "0.05", "stdout_lines": ["0.05"]}
+
+TASK [Display health status] ***************************************************
+ok: [openclaw.lab.home] => {
+ "msg": "═══════════════════════════════════════\nHost: openclaw.lab.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 13:18:49 up 2 days, 13:12, 2 users, load average: 0.05, 0.01, 0.00\nDisk Usage: 96%\nMemory Usage: 34.0%\nCPU Load: 0.05\nCPU Temp: N/A\n═══════════════════════════════════════\n"
+}
+
+PLAY RECAP *********************************************************************
+openclaw.lab.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
+
+```
+
+---
+*Généré automatiquement par Homelab Automation Dashboard*
+*Date: 2026-03-03T18:18:51.401631+00:00*