chore: auto-commit from dashboard

This commit is contained in:
2026-04-25 22:10:07 +00:00
parent d22ce49d76
commit 74d98553cc
4 changed files with 343 additions and 310 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

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