Compare commits

..

2 Commits

Author SHA1 Message Date
74d98553cc chore: auto-commit from dashboard 2026-04-25 22:10:07 +00:00
d22ce49d76 docs(kb): sync infrastructure with romfastsql proxmox config
LXC 171 mutat pe pveelite (nu pvemini), RAM 4GB (nu 16GB).
LXC 110 disk 8GB (nu 30GB), SSH user moltbot@.
Adăugat VM 302 (oracle-test, 10.0.20.130).
VM 201 extins cu detalii IIS, domenii, Win-ACME, ZFS replication.
VM 109 extins cu Oracle 19c, schedule backup RMAN.
Proxmox VE 8.4.14, storage cluster documentat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:05:09 +00:00
5 changed files with 442 additions and 332 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ credentials/
*.pid
memory.bak/
.use_openrouter
.gstack/

View File

@@ -155,6 +155,43 @@
color: #818cf8;
}
.status-section-icon.services {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.status-section-icon.sessions {
background: rgba(168, 85, 247, 0.15);
color: #a855f7;
}
.status-section-icon.logs {
background: rgba(156, 163, 175, 0.15);
color: #9ca3af;
}
.status-section-icon.doctor {
background: rgba(20, 184, 166, 0.15);
color: #14b8a6;
}
.status-section-actions {
display: flex;
gap: var(--space-2);
align-items: center;
margin-left: auto;
}
.status-section-actions .btn {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
}
/* Heavy content (services grid, sessions list, logs, doctor) — full width, no left indent */
.status-section-details.full-width {
padding: 0 var(--space-4) var(--space-3);
}
.status-section-info {
flex: 1;
min-width: 0;
@@ -904,44 +941,9 @@
}
/* ─────────────────────────────────────────────────────────
Eco panels (merged from eco.html)
Eco panels (merged from eco.html — now nested under Status)
───────────────────────────────────────────────────────── */
/* Collapsible section (used by Services/Git/Sessions/Logs/Doctor) */
.section {
margin-bottom: var(--space-4);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
cursor: pointer;
user-select: none;
}
.section.collapsed .section-body { display: none; }
.section.collapsed .section-header { margin-bottom: 0; }
.section.collapsed .sec-chev { transform: rotate(180deg); }
.sec-chev {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
flex-shrink: 0;
}
.section-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
/* Service cards */
.services-grid {
display: grid;
@@ -1309,100 +1311,6 @@
font-size: var(--text-xs);
}
/* Git panel (top-level) */
.git-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.git-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
cursor: pointer;
user-select: none;
}
.git-header:hover { background: var(--bg-elevated); }
.git-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
background: rgba(249, 115, 22, 0.15);
color: #f97316;
flex-shrink: 0;
}
.git-icon svg { width: 18px; height: 18px; }
.git-info { flex: 1; min-width: 0; }
.git-title {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
.git-subtitle {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
.git-badge {
padding: 3px 10px;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 600;
}
.git-badge.ok { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
.git-badge.warning { background: rgba(249, 115, 22, 0.15); color: #f97316; }
.git-badge.error { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
.git-actions {
display: flex;
gap: var(--space-2);
margin-left: auto;
}
.git-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.git-card.collapsed .git-toggle { transform: rotate(-90deg); }
.git-card.collapsed .git-details { display: none; }
.git-details {
padding: 0 var(--space-4) var(--space-3);
padding-left: calc(var(--space-4) + 32px + var(--space-3));
}
.git-detail-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 13px;
color: var(--text-primary);
padding: 2px 0;
}
.git-detail-item svg { width: 14px; height: 14px; color: var(--text-secondary); }
.git-detail-item.uncommitted { color: #f97316; }
/* Spinner */
.spinner {
display: inline-block;
@@ -1536,6 +1444,99 @@
<i data-lucide="chevron-down" class="status-toggle"></i>
</div>
<div class="status-content">
<!-- Git Section -->
<div class="status-section" id="gitSection">
<div class="status-section-header" onclick="toggleStatusSection('gitSection')">
<div class="status-section-icon git">
<i data-lucide="git-branch"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">
Git
<span class="status-badge ok" id="gitBadge">curat</span>
</div>
<div class="status-section-subtitle" id="gitSubtitle">Se încarcă...</div>
</div>
<div class="status-section-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary" onclick="ecoGitCommit()" title="Commit & Push" aria-label="Commit și push">
<i data-lucide="git-commit"></i> Commit
</button>
<button class="btn btn-secondary" onclick="loadGitStatus()" title="Refresh" aria-label="Refresh git status">
<i data-lucide="refresh-cw"></i>
</button>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details" id="gitDetails">
<div class="skeleton-row" style="width:70%"></div>
<div class="skeleton-row" style="width:40%"></div>
</div>
</div>
<!-- Services Section -->
<div class="status-section collapsed" id="servicesSection">
<div class="status-section-header" onclick="toggleStatusSection('servicesSection')">
<div class="status-section-icon services">
<i data-lucide="server"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">
Services
<span class="status-badge ok" id="servicesBadge"></span>
</div>
<div class="status-section-subtitle" id="servicesSubtitle">Se încarcă...</div>
</div>
<div class="status-section-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary" onclick="loadServices()" title="Refresh services" aria-label="Refresh services">
<i data-lucide="refresh-cw"></i>
</button>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details full-width">
<div class="services-grid" id="servicesGrid">
<div class="service-card"><div class="skeleton-row" style="width:60%"></div><div class="skeleton-row" style="width:80%"></div><div class="skeleton-row" style="width:40%"></div></div>
<div class="service-card"><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:60%"></div></div>
<div class="service-card"><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:55%"></div></div>
</div>
</div>
</div>
<!-- Sessions Section -->
<div class="status-section collapsed" id="sessionsSection">
<div class="status-section-header" onclick="toggleStatusSection('sessionsSection')">
<div class="status-section-icon sessions">
<i data-lucide="message-square"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">
Sessions
<span class="status-badge ok" id="sessionsBadge"></span>
</div>
<div class="status-section-subtitle" id="sessionsSubtitle">Canale active</div>
</div>
<div class="status-section-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary" onclick="loadSessions()" title="Refresh sessions" aria-label="Refresh sessions">
<i data-lucide="refresh-cw"></i>
</button>
<button class="btn btn-secondary btn-danger" onclick="clearAllSessions()" id="clearAllBtn" aria-label="Clear all sessions" style="display:none;">
<i data-lucide="trash-2"></i> Clear All
</button>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details full-width">
<div class="sessions-card">
<div id="sessionsContent">
<div style="padding:var(--space-3)">
<div class="skeleton-row" style="width:70%"></div>
<div class="skeleton-row" style="width:50%"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ANAF Section -->
<div class="status-section collapsed" id="anafSection">
<div class="status-section-header" onclick="toggleStatusSection('anafSection')">
@@ -1579,69 +1580,70 @@
</div>
</div>
</div>
</div>
<!-- Git Panel (promoted from Status Bar sub-section to top-level) -->
<div class="section" id="sec-git">
<div class="section-header" onclick="toggleSec('sec-git')">
<h2 class="section-title">
<i data-lucide="git-branch" style="width:18px;height:18px;"></i>
Git
</h2>
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="section-body">
<div class="git-card" id="gitCard">
<div class="git-header" onclick="toggleGitDetails()">
<div class="git-icon" aria-hidden="true">
<i data-lucide="git-branch"></i>
<!-- Logs Section (lazy-loaded) -->
<div class="status-section collapsed" id="logsSection" data-loaded="0">
<div class="status-section-header" onclick="toggleStatusSection('logsSection')">
<div class="status-section-icon logs">
<i data-lucide="scroll-text"></i>
</div>
<div class="git-info">
<div class="git-title">
Git
<span class="git-badge ok" id="gitBadge">curat</span>
<div class="status-section-info">
<div class="status-section-title">Logs</div>
<div class="status-section-subtitle">echo-core.service journalctl</div>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details full-width">
<div class="log-card">
<div class="log-toolbar">
<label for="logLines">Lines:</label>
<select id="logLines" onchange="loadLogs()">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
</select>
<button class="btn btn-secondary" onclick="loadLogs()" aria-label="Refresh logs" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
Refresh
</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:var(--space-2);">
<label for="autoRefreshLogs" style="font-size:var(--text-xs);color:var(--text-muted);">Auto</label>
<label class="toggle-switch">
<input type="checkbox" id="autoRefreshLogs" onchange="toggleAutoRefreshLogs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="git-subtitle" id="gitSubtitle">Se încarcă...</div>
<div class="log-content" id="logContent">Apasă Refresh sau expandă secțiunea pentru a încărca.</div>
</div>
<div class="git-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary" onclick="ecoGitCommit()" title="Commit & Push" aria-label="Commit și push" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="git-commit"></i> Commit
</button>
<button class="btn btn-secondary" onclick="loadGitStatus()" title="Refresh" aria-label="Refresh git status" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
</button>
</div>
<i data-lucide="chevron-down" class="git-toggle" aria-hidden="true"></i>
</div>
<div class="git-details" id="gitDetails">
<div class="skeleton-row" style="width:70%"></div>
<div class="skeleton-row" style="width:40%"></div>
</div>
</div>
</div>
</div>
<!-- Services Panel -->
<div class="section" id="sec-services">
<div class="section-header" onclick="toggleSec('sec-services')">
<h2 class="section-title">
<i data-lucide="server" style="width:18px;height:18px;"></i>
Services
</h2>
<div style="display:flex;gap:var(--space-2);align-items:center;">
<button class="btn btn-secondary" onclick="event.stopPropagation();loadServices()" title="Refresh services" aria-label="Refresh services" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
</button>
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<div class="section-body">
<div class="services-grid" id="servicesGrid">
<div class="service-card"><div class="skeleton-row" style="width:60%"></div><div class="skeleton-row" style="width:80%"></div><div class="skeleton-row" style="width:40%"></div></div>
<div class="service-card"><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:60%"></div></div>
<div class="service-card"><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:55%"></div></div>
<!-- Doctor Section -->
<div class="status-section collapsed" id="doctorSection">
<div class="status-section-header" onclick="toggleStatusSection('doctorSection')">
<div class="status-section-icon doctor">
<i data-lucide="stethoscope"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">Doctor</div>
<div class="status-section-subtitle">Diagnosticare sistem</div>
</div>
<div class="status-section-actions" onclick="event.stopPropagation()">
<button class="btn btn-primary" onclick="runDoctor()" id="doctorBtn" aria-label="Run diagnostics">
<i data-lucide="play"></i> Run
</button>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details full-width">
<div class="doctor-card" id="doctorCard">
<ul class="check-list" id="doctorChecks">
<li class="check-item" style="color:var(--text-muted);justify-content:center;">Apasă pentru a rula verificările.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
@@ -1701,93 +1703,6 @@
</div>
</div>
<!-- Sessions Panel -->
<div class="section" id="sec-sessions">
<div class="section-header" onclick="toggleSec('sec-sessions')">
<h2 class="section-title">
<i data-lucide="message-square" style="width:18px;height:18px;"></i>
Sessions
</h2>
<div style="display:flex;gap:var(--space-2);align-items:center;">
<button class="btn btn-secondary" onclick="event.stopPropagation();loadSessions()" title="Refresh sessions" aria-label="Refresh sessions" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
</button>
<button class="btn btn-secondary btn-danger" onclick="event.stopPropagation();clearAllSessions()" id="clearAllBtn" aria-label="Clear all sessions" style="display:none;font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="trash-2"></i> Clear All
</button>
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<div class="section-body">
<div class="sessions-card">
<div id="sessionsContent">
<div style="padding:var(--space-3)">
<div class="skeleton-row" style="width:70%"></div>
<div class="skeleton-row" style="width:50%"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Logs Panel (lazy-loaded) -->
<div class="section collapsed" id="sec-logs" data-loaded="0">
<div class="section-header" onclick="toggleSec('sec-logs')">
<h2 class="section-title">
<i data-lucide="scroll-text" style="width:18px;height:18px;"></i>
Logs
</h2>
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="section-body">
<div class="log-card">
<div class="log-toolbar">
<label for="logLines">Lines:</label>
<select id="logLines" onchange="loadLogs()">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
</select>
<button class="btn btn-secondary" onclick="loadLogs()" aria-label="Refresh logs" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
Refresh
</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:var(--space-2);">
<label for="autoRefreshLogs" style="font-size:var(--text-xs);color:var(--text-muted);">Auto</label>
<label class="toggle-switch">
<input type="checkbox" id="autoRefreshLogs" onchange="toggleAutoRefreshLogs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="log-content" id="logContent">Apasă Refresh sau expandă secțiunea pentru a încărca.</div>
</div>
</div>
</div>
<!-- Doctor Panel (lazy-loaded) -->
<div class="section collapsed" id="sec-doctor">
<div class="section-header" onclick="toggleSec('sec-doctor')">
<h2 class="section-title">
<i data-lucide="stethoscope" style="width:18px;height:18px;"></i>
Doctor
</h2>
<div style="display:flex;gap:var(--space-2);align-items:center;">
<button class="btn btn-primary" onclick="event.stopPropagation();runDoctor()" id="doctorBtn" aria-label="Run diagnostics" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="play"></i> Run
</button>
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<div class="section-body">
<div class="doctor-card" id="doctorCard">
<ul class="check-list" id="doctorChecks">
<li class="check-item" style="color:var(--text-muted);justify-content:center;">Apasă pentru a rula verificările.</li>
</ul>
</div>
</div>
</div>
</main>
<!-- Note Overlay -->
@@ -1999,9 +1914,10 @@
// Status sections collapse state
function getCollapsedStatusSections() {
const DEFAULTS = ['servicesSection', 'sessionsSection', 'anafSection', 'logsSection', 'doctorSection'];
try {
return JSON.parse(localStorage.getItem('collapsedStatusSections') || '["anafSection"]');
} catch { return ['anafSection']; }
return JSON.parse(localStorage.getItem('collapsedStatusSections') || JSON.stringify(DEFAULTS));
} catch { return DEFAULTS; }
}
function setCollapsedStatusSections(sections) {
@@ -2009,10 +1925,13 @@
}
function initStatusSections() {
const collapsed = getCollapsedStatusSections();
collapsed.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('collapsed');
const collapsed = new Set(getCollapsedStatusSections());
document.querySelectorAll('#statusBar .status-section').forEach(el => {
if (collapsed.has(el.id)) {
el.classList.add('collapsed');
} else {
el.classList.remove('collapsed');
}
});
}
@@ -2020,8 +1939,15 @@
const el = document.getElementById(id);
if (!el) return;
const wasCollapsed = el.classList.contains('collapsed');
el.classList.toggle('collapsed');
// Lazy-load logs the first time the section opens.
if (id === 'logsSection' && wasCollapsed && el.dataset.loaded !== '1') {
el.dataset.loaded = '1';
loadLogs();
}
const collapsed = getCollapsedStatusSections();
const idx = collapsed.indexOf(id);
if (el.classList.contains('collapsed')) {
@@ -2092,17 +2018,17 @@
if (git.clean) {
badge.textContent = 'curat';
badge.className = 'git-badge ok';
badge.className = 'status-badge ok';
} else {
badge.textContent = git.uncommittedCount + ' modificări';
badge.className = 'git-badge warning';
badge.className = 'status-badge warning';
}
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
const GITEA_URL = 'https://gitea.romfast.ro/romfast/echo-core';
let html = `
<div class="git-detail-item">
<div class="status-detail-item">
<i data-lucide="git-commit"></i>
<span><a href="${GITEA_URL}/commit/${git.lastCommit.hash}" target="_blank" style="color:var(--accent)">${git.lastCommit.hash}</a> ${git.lastCommit.message.substring(0, 50)}${git.lastCommit.message.length > 50 ? '...' : ''} (${git.lastCommit.time})</span>
</div>
@@ -2110,15 +2036,15 @@
if (git.uncommittedCount > 0) {
const files = (git.uncommittedParsed || []).slice(0, 5).map(f => f.path).join(', ');
const more = git.uncommittedCount > 5 ? ` +${git.uncommittedCount - 5}` : '';
html += `<div class="git-detail-item uncommitted">
html += `<div class="status-detail-item uncommitted">
<i data-lucide="alert-circle"></i>
<span><a href="/echo/files.html?git=1" style="color:var(--warning)"><strong>${git.uncommittedCount}</strong> necomise</a>: ${files}${more}</span>
</div>`;
}
if (git.diffStat) {
html += `<div class="git-detail-item" style="font-family:var(--font-mono);white-space:pre-wrap;"><i data-lucide="bar-chart-3"></i><span>${escapeHtml(git.diffStat)}</span></div>`;
html += `<div class="status-detail-item" style="font-family:var(--font-mono);white-space:pre-wrap;"><i data-lucide="bar-chart-3"></i><span>${escapeHtml(git.diffStat)}</span></div>`;
}
html += `<div class="git-detail-item">
html += `<div class="status-detail-item">
<i data-lucide="external-link"></i>
<a href="${GITEA_URL}" target="_blank" style="color:var(--accent)">gitea.romfast.ro/romfast/echo-core</a>
</div>`;
@@ -2129,7 +2055,7 @@
} catch (e) {
console.error('Git status error:', e);
badge.textContent = 'eroare';
badge.className = 'git-badge error';
badge.className = 'status-badge error';
subtitle.textContent = 'nu se poate încărca status-ul git';
details.innerHTML = `<div class="panel-error" role="alert"><i data-lucide="alert-triangle"></i><span>${escapeHtml(e.message || 'eroare necunoscută')}</span><button onclick="loadGitStatus()">Retry</button></div>`;
lucide.createIcons();
@@ -2882,22 +2808,6 @@
return div.innerHTML;
}
function toggleSec(id) {
const sec = document.getElementById(id);
if (!sec) return;
const wasCollapsed = sec.classList.contains('collapsed');
sec.classList.toggle('collapsed');
// Lazy-load logs when expanded for the first time.
if (id === 'sec-logs' && wasCollapsed && sec.dataset.loaded !== '1') {
sec.dataset.loaded = '1';
loadLogs();
}
}
function toggleGitDetails() {
document.getElementById('gitCard').classList.toggle('collapsed');
}
function formatUptime(seconds) {
if (!seconds && seconds !== 0) return '-';
const d = Math.floor(seconds / 86400);
@@ -2934,6 +2844,21 @@
function renderServices(services) {
const grid = document.getElementById('servicesGrid');
// Update the Status sub-section header (badge + subtitle).
const badge = document.getElementById('servicesBadge');
const subtitle = document.getElementById('servicesSubtitle');
if (badge && subtitle) {
const total = services.length;
const active = services.filter(s => s.active).length;
badge.textContent = `${active}/${total}`;
badge.className = active === total ? 'status-badge ok' : 'status-badge warning';
const stopped = services.filter(s => !s.active).map(s => svcLabel(s.name));
subtitle.textContent = stopped.length === 0
? 'Toate servicile rulează'
: `Oprite: ${stopped.join(', ')}`;
}
grid.innerHTML = services.map(svc => {
const isTaskboard = svc.name === 'echo-taskboard';
const canControl = !isTaskboard;
@@ -3099,6 +3024,21 @@
const container = document.getElementById('sessionsContent');
const clearBtn = document.getElementById('clearAllBtn');
// Update the Status sub-section header (badge + subtitle).
const badge = document.getElementById('sessionsBadge');
const subtitle = document.getElementById('sessionsSubtitle');
if (badge && subtitle) {
const count = sessions ? sessions.length : 0;
badge.textContent = String(count);
badge.className = 'status-badge ok';
if (count === 0) {
subtitle.textContent = 'Nicio sesiune activă';
} else {
const platforms = [...new Set(sessions.map(s => s.platform || 'unknown'))];
subtitle.textContent = platforms.join(', ');
}
}
if (!sessions || sessions.length === 0) {
container.innerHTML = '<div class="empty-state-warm">Nicio sesiune activă. Un DM către Echo pornește una.</div>';
clearBtn.style.display = 'none';
@@ -3218,7 +3158,12 @@
// ── Doctor ──────────────────────────────────────────────
async function runDoctor() {
document.getElementById('sec-doctor').classList.remove('collapsed');
// Expand both the parent Status bar and the Doctor sub-section so the user sees results.
document.getElementById('statusBar').classList.remove('collapsed');
const doctorSection = document.getElementById('doctorSection');
if (doctorSection.classList.contains('collapsed')) {
toggleStatusSection('doctorSection');
}
const list = document.getElementById('doctorChecks');
const btn = document.getElementById('doctorBtn');

View File

@@ -53,6 +53,21 @@
"video": "",
"tldr": "| Yann Tiersen | Comptine d'un autre été (Extended 1h) | https://www.youtube.com/watch?v=nJQV1jCM0gk |"
},
{
"file": "notes-data/youtube/2026-04-25_claude-code-playwright-automates-anything.md",
"title": "Claude Code + Playwright Automates Literally Anything",
"date": "2026-04-25",
"tags": [],
"domains": [
"work"
],
"types": [],
"category": "youtube",
"project": null,
"subdir": null,
"video": "",
"tldr": "Claude Code + Playwright CLI = automatizezi orice în browser, inclusiv în conturi unde ești logat. Se scrie un script Playwright, se testează, agentul învață din erori și îmbunătățește scriptul iterat..."
},
{
"file": "notes-data/tools/claude-agent-projects.md",
"title": "Proiecte pe LXC 171 — claude-agent",
@@ -8505,8 +8520,8 @@
"title": "Proiect: Vending Master - Integrare Website → ROA",
"date": "2026-01-30",
"tags": [
"integrare",
"vending-master"
"vending-master",
"integrare"
],
"domains": [
"work"
@@ -8986,9 +9001,9 @@
}
],
"stats": {
"total": 516,
"total": 517,
"by_domain": {
"work": 162,
"work": 163,
"health": 97,
"growth": 233,
"sprijin": 39,
@@ -9006,7 +9021,7 @@
"reflectii": 3,
"retete": 1,
"tools": 7,
"youtube": 104,
"youtube": 105,
"memory": 43
}
},

View File

@@ -1,6 +1,6 @@
# Infrastructură (Proxmox + Docker)
> Ultima actualizare: 2026-04-24. Scan complet al tuturor nodurilor.
> Ultima actualizare: 2026-04-25. Sync cu romfastsql/proxmox/ din Gitea.
## Acces rapid LXC
@@ -12,8 +12,8 @@
| 104 | flowise | pvemini | 10.0.20.161 | ❌ (publickey only) | `ssh echo@10.0.20.201 "sudo pct exec 104 -- bash"` |
| 106 | gitea | pvemini | 10.0.20.165 | — | `ssh echo@10.0.20.201 "sudo pct exec 106 -- sh"` ⚠️ Alpine (sh, nu bash) |
| 108 | central-oracle | pvemini | 10.0.20.121 | `ssh echo@10.0.20.121` | `ssh echo@10.0.20.201 "sudo pct exec 108 -- bash"` |
| 110 | moltbot | pveelite | 10.0.20.173 | `ssh echo@10.0.20.173` | `ssh echo@10.0.20.202 "sudo pct exec 110 -- bash"` |
| 171 | claude-agent | pvemini | 10.0.20.171 | `ssh user@10.0.20.171` | `ssh echo@10.0.20.201 "sudo pct exec 171 -- bash"` |
| 110 | moltbot | pveelite | 10.0.20.173 | `ssh moltbot@10.0.20.173` | `ssh echo@10.0.20.202 "sudo pct exec 110 -- bash"` |
| 171 | claude-agent | pveelite | 10.0.20.171 | `ssh claude@10.0.20.171` | `ssh echo@10.0.20.202 "sudo pct exec 171 -- bash"` |
---
@@ -193,9 +193,10 @@ ssh echo@10.0.20.201 "sudo pct exec 108 -- docker exec -it oracle-xe bash"
## LXC 110 — moltbot (pveelite)
- **IP:** 10.0.20.173 | **OS:** Debian/systemd | **Tailscale:** Da
- **Resurse:** 4GB RAM (564MB used) | 30GB disk (15GB used, 48%)
- **Acesta este LXC-ul pe care rulează echo-core**
- **IP:** 10.0.20.173 | **Tailscale IP:** 100.120.119.70 | **OS:** Debian/systemd | **Tailscale:** Da
- **Resurse:** 4GB RAM | 8GB disk (local-zfs) | 2 cores
- **SSH direct:** `ssh moltbot@10.0.20.173` (user dedicat non-root)
- **Acesta este LXC-ul pe care rulează echo-core (OpenClaw)**
**Servicii:**
| Serviciu | Port | Descriere |
@@ -208,10 +209,10 @@ ssh echo@10.0.20.201 "sudo pct exec 108 -- docker exec -it oracle-xe bash"
---
## LXC 171 — claude-agent (pvemini)
## LXC 171 — claude-agent (pveelite)
- **IP:** 10.0.20.171 | **Tailscale:** 100.95.55.51 | **OS:** Ubuntu/systemd
- **Resurse:** 16GB RAM (982MB used) | 32GB disk (23GB used, **72%** — de monitorizat)
- **IP:** 10.0.20.171 | **Tailscale:** 100.95.55.51 | **OS:** Ubuntu 24.04 LTS/systemd
- **Resurse:** 4GB RAM | 32GB disk (local-zfs) | 2 cores
- **User principal:** `claude` | **Workspace:** `/workspace/`
**Servicii:**
@@ -240,35 +241,111 @@ ssh echo@10.0.20.201 "sudo pct exec 108 -- docker exec -it oracle-xe bash"
**Depanare:**
```bash
ssh echo@10.0.20.201 "sudo pct exec 171 -- systemctl status code-server@claude ttyd"
ssh echo@10.0.20.201 "sudo pct exec 171 -- df -h /"
ssh echo@10.0.20.202 "sudo pct exec 171 -- systemctl status code-server@claude ttyd"
ssh echo@10.0.20.202 "sudo pct exec 171 -- df -h /"
```
---
## VM 201 — roacentral (pvemini)
- **IP:** 10.0.20.122 | **OS:** Windows | **QEMU Guest Agent:** Da
- Windows VM cu guest agent activ
- **VMID:** 201 | **Host:** pvemini | **Status:** Running (autostart)
- **OS:** Windows 11 Pro (24H2) | **QEMU Guest Agent:** Da
- **Resurse:** 2 cores | 4GB RAM | 500GB disk (local-zfs, ~89GB used)
- **Network:** virtio bridge (DHCP) | **RDP:** port 3389
## Mașini Windows externe (producție)
**Rol principal — Reverse Proxy IIS:**
| Domeniu | Destinație |
|---------|-----------|
| roa.romfast.ro | aplicație ROA |
| gitea.romfast.ro | LXC 106 |
| dokploy.romfast.ro | LXC 103 Traefik |
| roa-qr.romfast.ro | LXC 103 Traefik |
| *.roa.romfast.ro | Dokploy wildcard |
**Servicii instalate:**
- **IIS 10.0** — ASP.NET 4.8, WebSockets, URL Rewrite, SSL termination
- **Win-ACME v2.2.9** — certificate Let's Encrypt automate
- **Oracle Instant Client** — JDBC client pentru LXC 108
- **WinNUT** — UPS monitor (NUT server: 10.0.20.201:3493)
**Backup & Replicare:**
- Backup zilnic 02:00 (zstd comprimat)
- ZFS replication activă: pvemini → pve1 + pveelite (interval 30 min)
- HA dezactivat — pornire manuală la failover
---
## VM 109 — oracle-dr (pveelite)
- **VMID:** 109 | **Host:** pveelite | **Status:** Stopped (pornit doar pentru DR/test)
- **IP:** 10.0.20.37 | **OS:** Windows Server + Oracle 19c
- **HA group:** ha-prefer-pveelite | state=stopped, nofailback=1
- **Scop:** Disaster Recovery pentru Oracle Database (backup RMAN de pe server Windows extern)
**Oracle Database:**
- DB Name: ROA | Dimensiune: ~80 GB | Tabele: 42.625
- Strategie: full backup zilnic (6-7 GB) + cumulative incremental (200-300 MB)
**Schedule backup RMAN:**
| Oră | Tip |
|-----|-----|
| 02:30 | Full backup |
| 13:00 | Cumulative incremental |
| 18:00 | Cumulative incremental |
| 09:00 | Monitorizare automată |
**Depanare:**
```bash
ssh echo@10.0.20.202 "sudo qm status 109"
```
---
## VM 302 — oracle-test (pvemini)
- **VMID:** 302 | **Host:** pvemini | **Status:** Stopped (test on-demand)
- **IP:** 10.0.20.130 | **OS:** Windows 11
- **Resurse:** 4GB RAM | 500GB disk
- **Scop:** Mediu de test pentru scripturi instalare ROA pe Windows cu Oracle 21c XE
**Oracle Configuration:**
- Ediție: Oracle 21c XE (CDB/PDB) | Port: 1521 | Service: XEPDB1
- Setup dir: `C:\roa-setup\` | DMP files: `C:\DMPDIR\`
- Instalare completă: ~8 minute
**Depanare:**
```bash
ssh echo@10.0.20.201 "sudo qm status 302"
```
---
## Server Windows extern — producție
| Mașină | IP | Port | Rol |
|--------|----|------|-----|
| Oracle producție | 10.0.20.36 | 1521 | Oracle 10g Windows, baza de date principală ROA |
| Oracle DR | 10.0.20.37 | 1521 | Disaster recovery Oracle |
---
## Proxmox Noduri
**Versiune:** Proxmox VE 8.4.14 | **Cluster:** romfast (3 noduri, quorum activ)
**User:** `echo` | **Acces SSH:** `ssh echo@<IP>` | **Sudo:** `qm`, `pct`, `pvesh`
**Storage cluster:**
| Storage | Tip | Capacitate | Scop |
|---------|-----|------------|------|
| local-zfs | ZFS Pool | 1.75 TiB | Diskuri VM/LXC |
| backup | Directory | 1.79 TiB | Backup-uri (pvemini only) |
| local | Directory | 1.51 TiB | ISO-uri și template-uri |
### pvemini (10.0.20.201) — host principal
- **Resurse:** 64GB RAM, 1.4TB disk
- **LXC-uri:** 100, 103, 104, 105(stopped), 106, 108, 171
- **VM-uri:** 201(running), 300(stopped), 302(stopped)
- **Backup zilnic 02:00:** VM 100, 104, 106, 108, 171, 201 → storage "backup"
- **LXC-uri:** 100(running), 103(running), 104(running), 105(stopped), 106(running), 108(running)
- **VM-uri:** 201(running), 300(stopped — Windows 11 template), 302(stopped — oracle test)
- **Backup zilnic 02:00:** LXC 100, 104, 106, 108, VM 201 → storage "backup"
**Scripturi `/opt/scripts/`:**
- `ha-monitor.sh` — zilnic 00:00, status cluster HA
@@ -279,10 +356,10 @@ ssh echo@10.0.20.201 "sudo pct exec 171 -- df -h /"
- `vm107-monitor.sh` — monitorizare VM 107
### pveelite (10.0.20.202)
- **Resurse:** 16GB RAM, 557GB disk
- **LXC-uri:** 101(running), 105(stopped), 110(running), 301(stopped)
- **Resurse:** 16GB RAM, 557GB disk (+ 8GB ZFS swap)
- **LXC-uri:** 101(running), 105(stopped), 110(running), 171(running), 301(stopped)
- **VM-uri:** 109(stopped — oracle DR)
- **Backup zilnic 22:00:** LXC 101, 110 → backup-pvemini-nfs
- **Backup zilnic 22:00:** LXC 101, 110, 171 → backup-pvemini-nfs
**Scripturi `/opt/scripts/`:**
- `oracle-backup-monitor-proxmox.sh` — zilnic 21:00, verifică backup Oracle
@@ -306,7 +383,7 @@ ssh echo@10.0.20.201 "sudo pct exec 171 -- df -h /"
## Alertă automată când
- Container/VM down neașteptat
- Disk >85% utilizare (LXC 171 deja la 72% — monitorizez)
- Disk >85% utilizare pe orice container/VM
- Serviciu `unhealthy` >1h
- Erori repetate în logs

View File

@@ -0,0 +1,72 @@
# Claude Code + Playwright Automates Literally Anything
**Sursa:** https://www.youtube.com/watch?v=J-6pnl5DQg8
**Data:** 2026-04-25
**Durata:** 18:50
**Tags:** @work @automation @tools
---
## TL;DR
Claude Code + Playwright CLI = automatizezi orice în browser, inclusiv în conturi unde ești logat. Se scrie un script Playwright, se testează, agentul învață din erori și îmbunătățește scriptul iterativ. Use case-urile demo: QA automat pentru web app, scraping contacte (telefoane dentisti), și interacțiuni în platforme cu login (School). Se pot programa ca sarcini recurente headless.
---
## Puncte Cheie
1. **Playwright CLI vs MCP** — CLI consumă mult mai puțini tokeni decât MCP-ul Chrome DevTools (important pentru proiecte mari). Preferabil CLI.
2. **Flow de bază:**
- Instalezi Playwright CLI în proiect
- Claude Code scrie un script JS pentru automatizare
- Rulezi script → găsești buguri → Claude îl îmbunătățește → repeat
- Fiecare rulare face scriptul mai bun (agent învață din erori)
3. **QA automat:**
- Poți cere lui Claude să construiască un app și apoi să-l testeze singur
- Găsește buguri (ex: textarea cu Enter nu avansa), le fixează, rerulează
- Se poate transforma într-un **skill** → QA consistent și repetabil
4. **Sesiuni autentificate:**
- Opțiunea 1: Persistent browser profile (folosește datele Chrome existente)
- Prima rulare: login manual → sesiunea se salvează → rulările ulterioare = logged in automat
- Funcționează pe platforme "anti-automation" (ex: School/Skool)
5. **Skills recurente (autonome):**
- Se pot programa ca scheduled tasks pe desktop app
- Agentul AIS rulează automat: daily news roundup, likes pe wins, răspunsuri la notificări
- Headless = rulează fără să se vadă tab-ul
- Iterativ: când dă de un task nou (ex: vot pe poll) → creează un script nou → actualizează skill-ul
6. **Iterarea e normală:**
- Nu te aștepți să funcționeze din prima
- Course-correct cu instrucțiuni clare (ex: "thumbs up galben = deja likat, sari-l")
- 4-5 iterații → rezultat stabil
---
## Citate Notabile
> "When you give a tool as powerful as Claude Code access to literally control a browser, you can actually automate anything."
> "Every time that you use the script, it's going to get better."
> "The fact that you could automate QA by having multiple different headed or headless browsers spin up and running and fixing is a complete game changer."
---
## Idei & Aplicabilitate (Echo/Marius)
- **roa2web QA:** Claude Code + Playwright ar putea testa automat interfața web ROA — detectează buguri înainte să ajungă la clienți
- **ANAF Monitor:** Dacă pagina ANAF se schimbă UI și hash-ul nu mai e suficient, Playwright poate extrage conținut vizual
- **Automatizare rapoarte:** Descărcare automată de rapoarte din portale fără API (ex: bancare, fiscale)
- **Skill recurent:** Orice flux repetitiv în browser (login + download + procesare) se poate transforma în scheduled task
---
## Resurse Menționate
- Playwright CLI (alternativa la MCP Chrome DevTools)
- Alte CLI-uri: Forcell Agent Browser, Open CLI
- Video următor (din canal): Scheduled Tasks cu Claude Code