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
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:
parent
d29eefcef4
commit
57bfe02e32
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
18
app/main.js
18
app/main.js
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 }}
|
||||||
|
|||||||
@ -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*
|
||||||
@ -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*
|
||||||
@ -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*
|
||||||
Loading…
x
Reference in New Issue
Block a user