feat: Implement web-based SSH terminal with session management, command logging, and host interaction.
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled

This commit is contained in:
Bruno Charest 2026-03-03 13:37:52 -05:00
parent d29eefcef4
commit 57bfe02e32
10 changed files with 245 additions and 33 deletions

View File

@ -13,7 +13,10 @@ all:
env_lab: env_lab:
hosts: hosts:
dev.lab.home: null dev.lab.home: null
media-1.lab.home: null
media.labb.home: null media.labb.home: null
openclaw.lab.home:
ansible_host: 192.168.30.111
env_prod: env_prod:
hosts: hosts:
ali2v.truenas.home: null ali2v.truenas.home: null
@ -36,7 +39,9 @@ all:
dev.lab.home: null dev.lab.home: null
dev.prod.home: null dev.prod.home: null
jump.point.home: null jump.point.home: null
media-1.lab.home: null
media.labb.home: null media.labb.home: null
openclaw.lab.home: null
orangepi.pc.home: null orangepi.pc.home: null
raspi.4gb.home: null raspi.4gb.home: null
raspi.8gb.home: null raspi.8gb.home: null

View File

@ -139,6 +139,8 @@ class TerminalCommandLogRepository:
async def list_global( async def list_global(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
time_filter: str = "all",
pinned_only: bool = False,
host_id: Optional[str] = None, host_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
limit: int = 50, limit: int = 50,
@ -146,13 +148,6 @@ class TerminalCommandLogRepository:
) -> List[TerminalCommandLog]: ) -> List[TerminalCommandLog]:
""" """
List command logs globally with optional filters. 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] conditions = [TerminalCommandLog.is_blocked == False]
@ -165,6 +160,23 @@ class TerminalCommandLogRepository:
if query: if query:
conditions.append(TerminalCommandLog.command.ilike(f"%{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 = ( stmt = (
select(TerminalCommandLog) select(TerminalCommandLog)
.where(and_(*conditions)) .where(and_(*conditions))

View File

@ -3167,20 +3167,25 @@
/* Terminal History Panel */ /* Terminal History Panel */
.terminal-history-panel { .terminal-history-panel {
position: absolute;
top: 57px; /* Matches header height */
left: 0;
right: 0;
z-index: 100;
background: #1e1e2e; background: #1e1e2e;
border-bottom: 1px solid #374151; border-bottom: 1px solid #374151;
max-height: 350px; max-height: 0;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
opacity: 0; opacity: 0;
transform: translateY(-10px); transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease;
transition: opacity 0.2s ease, transform 0.2s ease; box-shadow: 0 5px 25px rgba(0,0,0,0.5);
} }
.terminal-history-panel.open { .terminal-history-panel.open {
max-height: 350px;
opacity: 1; opacity: 1;
transform: translateY(0);
} }
.terminal-history-header { .terminal-history-header {
@ -3308,7 +3313,11 @@
/* Docked mode: panel stays visible and doesn't close on command execute */ /* Docked mode: panel stays visible and doesn't close on command execute */
.terminal-history-panel.docked { .terminal-history-panel.docked {
position: relative;
top: 0;
max-height: 280px; max-height: 280px;
z-index: 1;
box-shadow: none;
border-bottom: 2px solid #7c3aed; border-bottom: 2px solid #7c3aed;
} }
@ -3396,6 +3405,17 @@
cursor: help; 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 { .terminal-history-count {
font-size: 0.625rem; font-size: 0.625rem;
color: #7c3aed; color: #7c3aed;

View File

@ -11726,6 +11726,8 @@ class DashboardManager {
showTerminalDrawer(hostName, hostIp, loading = false) { showTerminalDrawer(hostName, hostIp, loading = false) {
this.terminalDrawerOpen = true; this.terminalDrawerOpen = true;
this.terminalHistoryPanelOpen = false;
this.terminalHistoryPanelPinned = false;
// Create drawer if it doesn't exist // Create drawer if it doesn't exist
let drawer = document.getElementById('terminalDrawer'); let drawer = document.getElementById('terminalDrawer');
@ -11963,6 +11965,8 @@ class DashboardManager {
} }
this.terminalDrawerOpen = false; this.terminalDrawerOpen = false;
this.terminalHistoryPanelOpen = false;
this.terminalHistoryPanelPinned = false;
} }
// ===== TERMINAL CLEANUP HANDLERS ===== // ===== TERMINAL CLEANUP HANDLERS =====
@ -12260,7 +12264,7 @@ class DashboardManager {
async toggleTerminalHistory() { async toggleTerminalHistory() {
if (this.terminalHistoryPanelOpen) { if (this.terminalHistoryPanelOpen) {
this.closeTerminalHistoryPanel(); this.closeTerminalHistoryPanel(true);
} else { } else {
this.openTerminalHistoryPanel(); this.openTerminalHistoryPanel();
} }
@ -12292,9 +12296,9 @@ class DashboardManager {
} }
} }
closeTerminalHistoryPanel() { closeTerminalHistoryPanel(force = false) {
// If panel is pinned/docked, don't close on command execute // If panel is pinned/docked, don't close on command execute unless forced
if (this.terminalHistoryPanelPinned) return; if (this.terminalHistoryPanelPinned && !force) return;
const panel = document.getElementById('terminalHistoryPanel'); const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('terminalHistoryBtn'); const btn = document.getElementById('terminalHistoryBtn');
@ -12577,8 +12581,8 @@ class DashboardManager {
const newPinned = !cmd.is_pinned; const newPinned = !cmd.is_pinned;
try { try {
const hostId = this.terminalSession.host.id; const hId = cmd.host_id || this.terminalSession.host.id;
await this.apiCall(`/api/terminal/${hostId}/command-history/${cmd.command_hash}/pin`, { await this.apiCall(`/api/terminal/${hId}/command-history/${cmd.command_hash}/pin`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ is_pinned: newPinned }) body: JSON.stringify({ is_pinned: newPinned })
}); });
@ -12620,7 +12624,7 @@ class DashboardManager {
if (found) { if (found) {
found.term.focus(); found.term.focus();
found.term.paste(command); found.term.paste(command);
// Don't close panel (insert mode) this.closeTerminalHistoryPanel();
return; return;
} }

View File

@ -202,9 +202,8 @@ async def _verify_history_access(
if current_user: if current_user:
return True return True
token = _get_session_token_from_request(request, host_id, token=request.query_params.get("token")) # Try query param first (generic)
if not token: q_token = request.query_params.get("token")
return False
session_repo = TerminalSessionRepository(db_session) session_repo = TerminalSessionRepository(db_session)
# Try by ID first # Try by ID first
@ -218,7 +217,9 @@ async def _verify_history_access(
sessions = await session_repo.list_active_for_host(host.id) sessions = await session_repo.list_active_for_host(host.id)
for s in sessions: 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 True
return False return False
@ -1111,7 +1112,7 @@ async def get_terminal_connect_page(
// ---- Panel open/close ---- // ---- Panel open/close ----
function toggleHistory() { function toggleHistory() {
if (historyPanelOpen && !historyPanelPinned) { closeHistoryPanel(); return; } if (historyPanelOpen) { closeHistoryPanel(true); return; }
const panel = document.getElementById('terminalHistoryPanel'); const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('btnHistory'); const btn = document.getElementById('btnHistory');
panel.style.display = 'flex'; panel.classList.add('open'); btn.classList.add('active'); 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 (si) { si.focus(); si.select(); }
if (historyData.length === 0) loadHistory(); if (historyData.length === 0) loadHistory();
} }
function closeHistoryPanel() { function closeHistoryPanel(force = false) {
if (historyPanelPinned) return; if (historyPanelPinned && !force) return;
const panel = document.getElementById('terminalHistoryPanel'); const panel = document.getElementById('terminalHistoryPanel');
const btn = document.getElementById('btnHistory'); const btn = document.getElementById('btnHistory');
panel.classList.remove('open'); btn.classList.remove('active'); panel.classList.remove('open'); btn.classList.remove('active');
@ -1174,8 +1175,9 @@ async def get_terminal_connect_page(
const cmd = historyData[i]; const cmd = historyData[i];
if (!cmd || !cmd.command_hash) return; if (!cmd || !cmd.command_hash) return;
const newPinned = !cmd.is_pinned; const newPinned = !cmd.is_pinned;
const hId = cmd.host_id || HOST_ID;
try { 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', method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN }, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
body: JSON.stringify({ is_pinned: newPinned }) body: JSON.stringify({ is_pinned: newPinned })
@ -1214,10 +1216,10 @@ async def get_terminal_connect_page(
const list=document.getElementById('terminalHistoryList'),q=historySearchQuery||''; const list=document.getElementById('terminalHistoryList'),q=historySearchQuery||'';
if(!historyData.length){list.innerHTML=`<div class="terminal-history-empty"><i class="fas fa-terminal"></i><span>${q?`Aucun résultat pour "${escH(q)}"`:historyPinnedOnly?'Aucune commande épinglée':'Aucune commande'}</span></div>`;return;} if(!historyData.length){list.innerHTML=`<div class="terminal-history-empty"><i class="fas fa-terminal"></i><span>${q?`Aucun résultat pour "${escH(q)}"`:historyPinnedOnly?'Aucune commande épinglée':'Aucune commande'}</span></div>`;return;}
list.innerHTML=historyData.map((cmd,i)=>{ 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); let dc=escH(c.length>80?c.substring(0,80)+'...':c);
if(q)dc=dc.replace(new RegExp(`(${escRE(q)})`,'gi'),'<mark>$1</mark>'); if(q)dc=dc.replace(new RegExp(`(${escRE(q)})`,'gi'),'<mark>$1</mark>');
return `<div class="terminal-history-item${sel?' selected':''}" data-index="${i}" onclick="execH(${i})" title="${escH(c)}"><div class="terminal-history-cmd"><code>${pinned?'<i class="fas fa-thumbtack" style="color:#fbbf24;margin-right:4px;"></i>':''}${dc}</code></div><div class="terminal-history-meta"><span class="terminal-history-time">${ta}</span>${ec>1?`<span class="terminal-history-count">×${ec}</span>`:''}</div><div class="terminal-history-actions-inline"><button class="terminal-history-action" onclick="event.stopPropagation();togglePinH(${i})" title="${pinned?'Désépingler':'Épingler'}"><i class="fas fa-thumbtack" style="${pinned?'color:#fbbf24':''}"></i></button><button class="terminal-history-action" onclick="event.stopPropagation();copyH(${i})" title="Copier"><i class="fas fa-copy"></i></button><button class="terminal-history-action terminal-history-action-execute" onclick="event.stopPropagation();execH(${i})" title="Exécuter"><i class="fas fa-play"></i></button></div></div>`; return `<div class="terminal-history-item${sel?' selected':''}" data-index="${i}" onclick="execH(${i})" title="${escH(c)}"><div class="terminal-history-cmd"><code>${pinned?'<i class="fas fa-thumbtack" style="color:#fbbf24;margin-right:4px;"></i>':''}${dc}</code></div><div class="terminal-history-meta"><span class="terminal-history-time">${ta}</span>${ec>1?`<span class="terminal-history-count">×${ec}</span>`:''}${hn?`<span class="terminal-history-host">${escH(hn)}</span>`:''}</div><div class="terminal-history-actions-inline"><button class="terminal-history-action" onclick="event.stopPropagation();togglePinH(${i})" title="${pinned?'Désépingler':'Épingler'}"><i class="fas fa-thumbtack" style="${pinned?'color:#fbbf24':''}"></i></button><button class="terminal-history-action" onclick="event.stopPropagation();copyH(${i})" title="Copier"><i class="fas fa-copy"></i></button><button class="terminal-history-action terminal-history-action-execute" onclick="event.stopPropagation();execH(${i})" title="Exécuter"><i class="fas fa-play"></i></button></div></div>`;
}).join(''); }).join('');
if(historySelectedIndex>=0){const s=list.querySelector('.selected');if(s)s.scrollIntoView({block:'nearest',behavior:'smooth'});} 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( CommandHistoryItem(
id=log.id, id=log.id,
command=log.command, command=log.command,
command_hash=log.command_hash,
created_at=log.created_at, created_at=log.created_at,
host_id=log.host_id,
host_name=log.host_name, host_name=log.host_name,
username=log.username, username=log.username,
is_pinned=log.is_pinned,
) )
for log in logs for log in logs
] ]
@ -1597,6 +1602,8 @@ async def toggle_command_pin(
async def get_global_command_history( async def get_global_command_history(
request: Request, request: Request,
query: Optional[str] = None, query: Optional[str] = None,
time_filter: str = "all",
pinned_only: bool = False,
host_id: Optional[str] = None, host_id: Optional[str] = None,
limit: int = 50, limit: int = 50,
offset: int = 0, offset: int = 0,
@ -1623,6 +1630,8 @@ async def get_global_command_history(
cmd_repo = TerminalCommandLogRepository(db_session) cmd_repo = TerminalCommandLogRepository(db_session)
logs = await cmd_repo.list_global( logs = await cmd_repo.list_global(
query=query, query=query,
time_filter=time_filter,
pinned_only=pinned_only,
host_id=host_id, host_id=host_id,
user_id=str(user_id) if user_id else None, user_id=str(user_id) if user_id else None,
limit=min(limit, 100), limit=min(limit, 100),
@ -1633,9 +1642,12 @@ async def get_global_command_history(
CommandHistoryItem( CommandHistoryItem(
id=log.id, id=log.id,
command=log.command, command=log.command,
command_hash=log.command_hash,
created_at=log.created_at, created_at=log.created_at,
host_id=log.host_id,
host_name=log.host_name, host_name=log.host_name,
username=log.username, username=log.username,
is_pinned=log.is_pinned,
) )
for log in logs for log in logs
] ]

View File

@ -15,7 +15,9 @@ class CommandHistoryItem(BaseModel):
"""A single command from history.""" """A single command from history."""
id: int id: int
command: str command: str
command_hash: Optional[str] = None
created_at: datetime created_at: datetime
host_id: Optional[str] = None
host_name: Optional[str] = None host_name: Optional[str] = None
username: Optional[str] = None username: Optional[str] = None
execution_count: Optional[int] = None execution_count: Optional[int] = None

View File

@ -191,6 +191,12 @@
} }
.terminal-history-meta { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } .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-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 { .terminal-history-count {
font-size: 0.625rem; color: #7c3aed; font-size: 0.625rem; color: #7c3aed;
background: rgba(124, 58, 237, 0.2); padding: 0.125rem 0.375rem; background: rgba(124, 58, 237, 0.2); padding: 0.125rem 0.375rem;
@ -289,11 +295,6 @@
{% if debug_panel_html %} {% if debug_panel_html %}
{{ debug_panel_html | safe }} {{ debug_panel_html | safe }}
{% endif %} {% endif %}
<iframe
id="terminalFrame"
src="about:blank"
allow="clipboard-read; clipboard-write; clipboard-write-text"
></iframe>
<div class="terminal-history-panel" id="terminalHistoryPanel" style="display: none;"> <div class="terminal-history-panel" id="terminalHistoryPanel" style="display: none;">
<div class="terminal-history-header"> <div class="terminal-history-header">
<div class="terminal-history-search"> <div class="terminal-history-search">
@ -338,6 +339,11 @@
</span> </span>
</div> </div>
</div> </div>
<iframe
id="terminalFrame"
src="about:blank"
allow="clipboard-read; clipboard-write; clipboard-write-text"
></iframe>
</div> </div>
{{ script_block | safe }} {{ script_block | safe }}

View File

@ -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*

View File

@ -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*

View File

@ -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*