chore: auto-commit from dashboard
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ credentials/
|
|||||||
*.pid
|
*.pid
|
||||||
memory.bak/
|
memory.bak/
|
||||||
.use_openrouter
|
.use_openrouter
|
||||||
|
.gstack/
|
||||||
|
|||||||
@@ -155,6 +155,43 @@
|
|||||||
color: #818cf8;
|
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 {
|
.status-section-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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 */
|
/* Service cards */
|
||||||
.services-grid {
|
.services-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1309,100 +1311,6 @@
|
|||||||
font-size: var(--text-xs);
|
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 */
|
||||||
.spinner {
|
.spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -1536,6 +1444,99 @@
|
|||||||
<i data-lucide="chevron-down" class="status-toggle"></i>
|
<i data-lucide="chevron-down" class="status-toggle"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-content">
|
<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 -->
|
<!-- ANAF Section -->
|
||||||
<div class="status-section collapsed" id="anafSection">
|
<div class="status-section collapsed" id="anafSection">
|
||||||
<div class="status-section-header" onclick="toggleStatusSection('anafSection')">
|
<div class="status-section-header" onclick="toggleStatusSection('anafSection')">
|
||||||
@@ -1579,69 +1580,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Logs Section (lazy-loaded) -->
|
||||||
</div>
|
<div class="status-section collapsed" id="logsSection" data-loaded="0">
|
||||||
|
<div class="status-section-header" onclick="toggleStatusSection('logsSection')">
|
||||||
<!-- Git Panel (promoted from Status Bar sub-section to top-level) -->
|
<div class="status-section-icon logs">
|
||||||
<div class="section" id="sec-git">
|
<i data-lucide="scroll-text"></i>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="git-info">
|
<div class="status-section-info">
|
||||||
<div class="git-title">
|
<div class="status-section-title">Logs</div>
|
||||||
Git
|
<div class="status-section-subtitle">echo-core.service journalctl</div>
|
||||||
<span class="git-badge ok" id="gitBadge">curat</span>
|
</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>
|
||||||
<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>
|
||||||
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Services Panel -->
|
<!-- Doctor Section -->
|
||||||
<div class="section" id="sec-services">
|
<div class="status-section collapsed" id="doctorSection">
|
||||||
<div class="section-header" onclick="toggleSec('sec-services')">
|
<div class="status-section-header" onclick="toggleStatusSection('doctorSection')">
|
||||||
<h2 class="section-title">
|
<div class="status-section-icon doctor">
|
||||||
<i data-lucide="server" style="width:18px;height:18px;"></i>
|
<i data-lucide="stethoscope"></i>
|
||||||
Services
|
</div>
|
||||||
</h2>
|
<div class="status-section-info">
|
||||||
<div style="display:flex;gap:var(--space-2);align-items:center;">
|
<div class="status-section-title">Doctor</div>
|
||||||
<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);">
|
<div class="status-section-subtitle">Diagnosticare sistem</div>
|
||||||
<i data-lucide="refresh-cw"></i>
|
</div>
|
||||||
</button>
|
<div class="status-section-actions" onclick="event.stopPropagation()">
|
||||||
<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>
|
<button class="btn btn-primary" onclick="runDoctor()" id="doctorBtn" aria-label="Run diagnostics">
|
||||||
</div>
|
<i data-lucide="play"></i> Run
|
||||||
</div>
|
</button>
|
||||||
<div class="section-body">
|
</div>
|
||||||
<div class="services-grid" id="servicesGrid">
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
||||||
<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>
|
||||||
<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="status-section-details full-width">
|
||||||
<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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1701,93 +1703,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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>
|
</main>
|
||||||
|
|
||||||
<!-- Note Overlay -->
|
<!-- Note Overlay -->
|
||||||
@@ -1999,9 +1914,10 @@
|
|||||||
|
|
||||||
// Status sections collapse state
|
// Status sections collapse state
|
||||||
function getCollapsedStatusSections() {
|
function getCollapsedStatusSections() {
|
||||||
|
const DEFAULTS = ['servicesSection', 'sessionsSection', 'anafSection', 'logsSection', 'doctorSection'];
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem('collapsedStatusSections') || '["anafSection"]');
|
return JSON.parse(localStorage.getItem('collapsedStatusSections') || JSON.stringify(DEFAULTS));
|
||||||
} catch { return ['anafSection']; }
|
} catch { return DEFAULTS; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCollapsedStatusSections(sections) {
|
function setCollapsedStatusSections(sections) {
|
||||||
@@ -2009,19 +1925,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initStatusSections() {
|
function initStatusSections() {
|
||||||
const collapsed = getCollapsedStatusSections();
|
const collapsed = new Set(getCollapsedStatusSections());
|
||||||
collapsed.forEach(id => {
|
document.querySelectorAll('#statusBar .status-section').forEach(el => {
|
||||||
const el = document.getElementById(id);
|
if (collapsed.has(el.id)) {
|
||||||
if (el) el.classList.add('collapsed');
|
el.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
el.classList.remove('collapsed');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleStatusSection(id) {
|
function toggleStatusSection(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
const wasCollapsed = el.classList.contains('collapsed');
|
||||||
el.classList.toggle('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 collapsed = getCollapsedStatusSections();
|
||||||
const idx = collapsed.indexOf(id);
|
const idx = collapsed.indexOf(id);
|
||||||
if (el.classList.contains('collapsed')) {
|
if (el.classList.contains('collapsed')) {
|
||||||
@@ -2092,17 +2018,17 @@
|
|||||||
|
|
||||||
if (git.clean) {
|
if (git.clean) {
|
||||||
badge.textContent = 'curat';
|
badge.textContent = 'curat';
|
||||||
badge.className = 'git-badge ok';
|
badge.className = 'status-badge ok';
|
||||||
} else {
|
} else {
|
||||||
badge.textContent = git.uncommittedCount + ' modificări';
|
badge.textContent = git.uncommittedCount + ' modificări';
|
||||||
badge.className = 'git-badge warning';
|
badge.className = 'status-badge warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
|
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
|
||||||
|
|
||||||
const GITEA_URL = 'https://gitea.romfast.ro/romfast/echo-core';
|
const GITEA_URL = 'https://gitea.romfast.ro/romfast/echo-core';
|
||||||
let html = `
|
let html = `
|
||||||
<div class="git-detail-item">
|
<div class="status-detail-item">
|
||||||
<i data-lucide="git-commit"></i>
|
<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>
|
<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>
|
</div>
|
||||||
@@ -2110,15 +2036,15 @@
|
|||||||
if (git.uncommittedCount > 0) {
|
if (git.uncommittedCount > 0) {
|
||||||
const files = (git.uncommittedParsed || []).slice(0, 5).map(f => f.path).join(', ');
|
const files = (git.uncommittedParsed || []).slice(0, 5).map(f => f.path).join(', ');
|
||||||
const more = git.uncommittedCount > 5 ? ` +${git.uncommittedCount - 5}` : '';
|
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>
|
<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>
|
<span><a href="/echo/files.html?git=1" style="color:var(--warning)"><strong>${git.uncommittedCount}</strong> necomise</a>: ${files}${more}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (git.diffStat) {
|
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>
|
<i data-lucide="external-link"></i>
|
||||||
<a href="${GITEA_URL}" target="_blank" style="color:var(--accent)">gitea.romfast.ro/romfast/echo-core</a>
|
<a href="${GITEA_URL}" target="_blank" style="color:var(--accent)">gitea.romfast.ro/romfast/echo-core</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -2129,7 +2055,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Git status error:', e);
|
console.error('Git status error:', e);
|
||||||
badge.textContent = 'eroare';
|
badge.textContent = 'eroare';
|
||||||
badge.className = 'git-badge error';
|
badge.className = 'status-badge error';
|
||||||
subtitle.textContent = 'nu se poate încărca status-ul git';
|
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>`;
|
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();
|
lucide.createIcons();
|
||||||
@@ -2882,22 +2808,6 @@
|
|||||||
return div.innerHTML;
|
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) {
|
function formatUptime(seconds) {
|
||||||
if (!seconds && seconds !== 0) return '-';
|
if (!seconds && seconds !== 0) return '-';
|
||||||
const d = Math.floor(seconds / 86400);
|
const d = Math.floor(seconds / 86400);
|
||||||
@@ -2934,6 +2844,21 @@
|
|||||||
|
|
||||||
function renderServices(services) {
|
function renderServices(services) {
|
||||||
const grid = document.getElementById('servicesGrid');
|
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 => {
|
grid.innerHTML = services.map(svc => {
|
||||||
const isTaskboard = svc.name === 'echo-taskboard';
|
const isTaskboard = svc.name === 'echo-taskboard';
|
||||||
const canControl = !isTaskboard;
|
const canControl = !isTaskboard;
|
||||||
@@ -3099,6 +3024,21 @@
|
|||||||
const container = document.getElementById('sessionsContent');
|
const container = document.getElementById('sessionsContent');
|
||||||
const clearBtn = document.getElementById('clearAllBtn');
|
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) {
|
if (!sessions || sessions.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-state-warm">Nicio sesiune activă. Un DM către Echo pornește una.</div>';
|
container.innerHTML = '<div class="empty-state-warm">Nicio sesiune activă. Un DM către Echo pornește una.</div>';
|
||||||
clearBtn.style.display = 'none';
|
clearBtn.style.display = 'none';
|
||||||
@@ -3218,7 +3158,12 @@
|
|||||||
// ── Doctor ──────────────────────────────────────────────
|
// ── Doctor ──────────────────────────────────────────────
|
||||||
|
|
||||||
async function runDoctor() {
|
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 list = document.getElementById('doctorChecks');
|
||||||
const btn = document.getElementById('doctorBtn');
|
const btn = document.getElementById('doctorBtn');
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,21 @@
|
|||||||
"video": "",
|
"video": "",
|
||||||
"tldr": "| Yann Tiersen | Comptine d'un autre été (Extended 1h) | https://www.youtube.com/watch?v=nJQV1jCM0gk |"
|
"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",
|
"file": "notes-data/tools/claude-agent-projects.md",
|
||||||
"title": "Proiecte pe LXC 171 — claude-agent",
|
"title": "Proiecte pe LXC 171 — claude-agent",
|
||||||
@@ -8505,8 +8520,8 @@
|
|||||||
"title": "Proiect: Vending Master - Integrare Website → ROA",
|
"title": "Proiect: Vending Master - Integrare Website → ROA",
|
||||||
"date": "2026-01-30",
|
"date": "2026-01-30",
|
||||||
"tags": [
|
"tags": [
|
||||||
"integrare",
|
"vending-master",
|
||||||
"vending-master"
|
"integrare"
|
||||||
],
|
],
|
||||||
"domains": [
|
"domains": [
|
||||||
"work"
|
"work"
|
||||||
@@ -8986,9 +9001,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stats": {
|
"stats": {
|
||||||
"total": 516,
|
"total": 517,
|
||||||
"by_domain": {
|
"by_domain": {
|
||||||
"work": 162,
|
"work": 163,
|
||||||
"health": 97,
|
"health": 97,
|
||||||
"growth": 233,
|
"growth": 233,
|
||||||
"sprijin": 39,
|
"sprijin": 39,
|
||||||
@@ -9006,7 +9021,7 @@
|
|||||||
"reflectii": 3,
|
"reflectii": 3,
|
||||||
"retete": 1,
|
"retete": 1,
|
||||||
"tools": 7,
|
"tools": 7,
|
||||||
"youtube": 104,
|
"youtube": 105,
|
||||||
"memory": 43
|
"memory": 43
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user