Status bar v2: expandable sections with live Git status

- Git section: branch, last commit, uncommitted files with API
- ANAF section: collapsible with last check time
- Cron section: shows today's jobs with done/pending status
- Refresh button + auto-refresh on page focus
- New /api/git endpoint for live git status
- All sections collapsible with localStorage persistence
This commit is contained in:
Echo
2026-01-30 22:19:46 +00:00
parent 1b3b6d33ef
commit 838c38e82f
9 changed files with 614 additions and 121 deletions

View File

@@ -69,6 +69,12 @@
color: #22c55e;
}
.status-actions {
display: flex;
gap: var(--space-2);
margin-left: auto;
}
.status-summary {
flex: 1;
font-size: var(--text-xs);
@@ -92,64 +98,186 @@
}
.status-content {
padding: var(--space-3) var(--space-4);
padding: 0;
}
.status-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-bottom: var(--space-2);
/* Status sections */
.status-section {
border-bottom: 1px solid var(--border);
}
.status-item {
.status-section:last-child {
border-bottom: none;
}
.status-section-header {
display: flex;
align-items: center;
gap: var(--space-1);
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
cursor: pointer;
user-select: none;
transition: background var(--transition-fast);
}
.status-section-header:hover {
background: var(--bg-elevated);
}
.status-section-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
flex-shrink: 0;
}
.status-section-icon svg {
width: 18px;
height: 18px;
}
.status-section-icon.git {
background: rgba(249, 115, 22, 0.15);
color: #f97316;
}
.status-section-icon.anaf {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.status-section-icon.cron {
background: rgba(99, 102, 241, 0.15);
color: #818cf8;
}
.status-section-info {
flex: 1;
min-width: 0;
}
.status-section-title {
font-size: var(--text-sm);
}
.status-label {
color: var(--text-muted);
}
.status-value {
font-weight: 600;
color: var(--text-primary);
}
.status-value.ok { color: #22c55e; }
.status-value.warning { color: #f59e0b; }
.status-value.error { color: #ef4444; }
.status-time {
font-size: var(--text-xs);
color: var(--text-muted);
margin-left: var(--space-1);
}
.cron-row {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.cron-label {
.status-section-subtitle {
font-size: var(--text-xs);
color: var(--text-muted);
margin-top: 2px;
}
.status-badge {
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: 600;
}
.status-badge.ok {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.status-badge.warning {
background: rgba(249, 115, 22, 0.15);
color: #f97316;
}
.status-badge.error {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.status-section-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.status-section.collapsed .status-section-toggle {
transform: rotate(-90deg);
}
.status-section.collapsed .status-section-details {
display: none;
}
.status-section-details {
padding: 0 var(--space-4) var(--space-3);
padding-left: calc(var(--space-4) + 32px + var(--space-3));
}
.status-detail-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--text-secondary);
padding: var(--space-1) 0;
}
.status-detail-item svg {
width: 12px;
height: 12px;
color: var(--text-muted);
}
.cron-list {
.status-detail-item.uncommitted {
color: #f97316;
}
.status-detail-item code {
font-family: monospace;
background: var(--bg-elevated);
padding: 1px 4px;
border-radius: 2px;
font-size: 11px;
}
/* Cron items */
.cron-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
padding: var(--space-1) 0;
}
.cron-item.done {
color: var(--text-muted);
}
.cron-item.done .cron-name {
text-decoration: line-through;
}
.cron-item.pending {
color: var(--text-secondary);
}
.cron-done {
color: var(--text-muted);
text-decoration: line-through;
.cron-time {
font-family: monospace;
min-width: 45px;
}
.cron-icon {
width: 14px;
height: 14px;
}
.cron-icon.done { color: #22c55e; }
.cron-icon.pending { color: var(--text-muted); }
.cron-icon.failed { color: #ef4444; }
/* Two-column dashboard */
.dashboard-grid {
display: grid;
@@ -696,27 +824,75 @@
<span>Status</span>
</div>
<div class="status-summary" id="statusSummary">Se încarcă...</div>
<div class="status-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary btn-sm" onclick="refreshStatus()" title="Refresh">
<i data-lucide="refresh-cw"></i>
</button>
</div>
<i data-lucide="chevron-down" class="status-toggle"></i>
</div>
<div class="status-content">
<div class="status-row">
<div class="status-item">
<span class="status-label">ANAF:</span>
<span class="status-value" id="anafStatus">-</span>
<!-- 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>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-item">
<span class="status-label">Git:</span>
<span class="status-value" id="gitStatus">-</span>
</div>
<div class="status-item">
<span class="status-label">Raport:</span>
<span class="status-value" id="lastReport">-</span>
<span class="status-time" id="reportTime"></span>
<div class="status-section-details" id="gitDetails">
<!-- Populated by JS -->
</div>
</div>
<div class="cron-row">
<span class="cron-label">Cron azi:</span>
<span class="cron-list" id="cronList">-</span>
<!-- ANAF Section -->
<div class="status-section collapsed" id="anafSection">
<div class="status-section-header" onclick="toggleStatusSection('anafSection')">
<div class="status-section-icon anaf">
<i data-lucide="building-2"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">
ANAF Monitor
<span class="status-badge ok" id="anafBadge">OK</span>
</div>
<div class="status-section-subtitle" id="anafSubtitle">Nicio modificare detectată</div>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details" id="anafDetails">
<div class="status-detail-item">
<i data-lucide="clock"></i>
<span id="anafLastCheck">Ultima verificare: -</span>
</div>
</div>
</div>
<!-- Cron Section -->
<div class="status-section" id="cronSection">
<div class="status-section-header" onclick="toggleStatusSection('cronSection')">
<div class="status-section-icon cron">
<i data-lucide="clock"></i>
</div>
<div class="status-section-info">
<div class="status-section-title">
Cron Jobs
<span class="status-badge ok" id="cronBadge">0/0</span>
</div>
<div class="status-section-subtitle" id="cronSubtitle">Jobs programate azi</div>
</div>
<i data-lucide="chevron-down" class="status-section-toggle"></i>
</div>
<div class="status-section-details" id="cronDetails">
<!-- Populated by JS -->
</div>
</div>
</div>
</div>
@@ -911,64 +1087,205 @@
const priorityOrder = ['urgent-important', 'important', 'urgent', 'backlog'];
// Status
// Status sections collapse state
function getCollapsedStatusSections() {
try {
return JSON.parse(localStorage.getItem('collapsedStatusSections') || '["anafSection"]');
} catch { return ['anafSection']; }
}
function setCollapsedStatusSections(sections) {
localStorage.setItem('collapsedStatusSections', JSON.stringify(sections));
}
function initStatusSections() {
const collapsed = getCollapsedStatusSections();
collapsed.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('collapsed');
});
}
function toggleStatusSection(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('collapsed');
const collapsed = getCollapsedStatusSections();
const idx = collapsed.indexOf(id);
if (el.classList.contains('collapsed')) {
if (idx === -1) collapsed.push(id);
} else {
if (idx > -1) collapsed.splice(idx, 1);
}
setCollapsedStatusSections(collapsed);
}
// Status loading
async function loadStatus() {
await Promise.all([
loadGitStatus(),
loadAnafStatus(),
loadCronStatus()
]);
updateStatusSummary();
}
async function refreshStatus() {
showToast('Se reîmprospătează...');
await loadStatus();
showToast('Status actualizat!');
}
async function loadGitStatus() {
try {
const response = await fetch('/api/git?' + Date.now());
if (!response.ok) throw new Error('API error');
const git = await response.json();
// Update badge
const badge = document.getElementById('gitBadge');
if (git.clean) {
badge.textContent = 'curat';
badge.className = 'status-badge ok';
} else {
badge.textContent = git.uncommittedCount + ' modificări';
badge.className = 'status-badge warning';
}
// Update subtitle
const subtitle = document.getElementById('gitSubtitle');
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
// Update details
const details = document.getElementById('gitDetails');
let html = `
<div class="status-detail-item">
<i data-lucide="git-branch"></i>
<span>Branch: <strong>${git.branch}</strong></span>
</div>
<div class="status-detail-item">
<i data-lucide="git-commit"></i>
<span>Last: <code>${git.lastCommit.hash}</code> ${git.lastCommit.message} (${git.lastCommit.time})</span>
</div>
`;
if (git.uncommittedCount > 0) {
html += `<div class="status-detail-item uncommitted">
<i data-lucide="alert-circle"></i>
<span><strong>${git.uncommittedCount}</strong> fișiere necomise:</span>
</div>`;
git.uncommitted.slice(0, 5).forEach(file => {
html += `<div class="status-detail-item uncommitted">
<i data-lucide="file"></i>
<code>${file}</code>
</div>`;
});
if (git.uncommittedCount > 5) {
html += `<div class="status-detail-item uncommitted">
<span>... și încă ${git.uncommittedCount - 5}</span>
</div>`;
}
} else {
html += `<div class="status-detail-item">
<i data-lucide="check-circle"></i>
<span>Totul comis ✓</span>
</div>`;
}
details.innerHTML = html;
lucide.createIcons();
return git;
} catch (e) {
console.error('Git status error:', e);
document.getElementById('gitBadge').textContent = 'eroare';
document.getElementById('gitBadge').className = 'status-badge error';
}
}
async function loadAnafStatus() {
try {
const response = await fetch('status.json?' + Date.now());
if (response.ok) {
const status = await response.json();
updateStatus(status);
if (!response.ok) throw new Error('No status.json');
const status = await response.json();
if (status.anaf) {
const badge = document.getElementById('anafBadge');
badge.textContent = status.anaf.status || 'OK';
badge.className = 'status-badge ' + (status.anaf.ok !== false ? 'ok' : 'warning');
const subtitle = document.getElementById('anafSubtitle');
subtitle.textContent = status.anaf.message || 'Nicio modificare detectată';
if (status.anaf.lastCheck) {
document.getElementById('anafLastCheck').textContent =
'Ultima verificare: ' + status.anaf.lastCheck;
}
}
return status;
} catch (e) {
console.log('No status.json');
console.log('No ANAF status');
}
updateCronList();
}
function updateStatus(status) {
const anafEl = document.getElementById('anafStatus');
if (status.anaf) {
anafEl.textContent = status.anaf.status;
anafEl.className = 'status-value ' + (status.anaf.ok ? 'ok' : 'warning');
}
const gitEl = document.getElementById('gitStatus');
if (status.git) {
gitEl.textContent = status.git.status;
gitEl.className = 'status-value ' + (status.git.clean ? 'ok' : 'warning');
}
const reportEl = document.getElementById('lastReport');
const timeEl = document.getElementById('reportTime');
if (status.lastReport) {
reportEl.textContent = status.lastReport.summary || 'OK';
timeEl.textContent = status.lastReport.time ? '(' + status.lastReport.time + ')' : '';
}
const summaryEl = document.getElementById('statusSummary');
let summary = [];
if (status.anaf) summary.push('ANAF: ' + status.anaf.status);
if (status.git) summary.push('Git: ' + status.git.status);
summaryEl.textContent = summary.join(' · ') || 'OK';
}
function updateCronList() {
async function loadCronStatus() {
const now = new Date();
const hour = now.getHours();
// TODO: În viitor, fetch din /api/cron
const jobs = [
{ time: '07:30', name: 'coaching', done: hour >= 8 },
{ time: '08:30', name: 'raport', done: hour >= 9 },
{ time: '20:00', name: 'raport', done: hour >= 20 },
{ time: '21:00', name: 'coaching', done: hour >= 21 }
{ time: '06:30', name: 'morning-report', done: hour >= 7 },
{ time: '07:00', name: 'morning-coaching', done: hour >= 7 },
{ time: '18:00', name: 'evening-report', done: hour >= 18 },
{ time: '19:00', name: 'evening-coaching', done: hour >= 19 }
];
const listEl = document.getElementById('cronList');
listEl.innerHTML = jobs.map(job =>
`<span class="${job.done ? 'cron-done' : ''}">${job.time} ${job.name}</span>`
).join(' · ');
const doneCount = jobs.filter(j => j.done).length;
// Update badge
const badge = document.getElementById('cronBadge');
badge.textContent = `${doneCount}/${jobs.length}`;
badge.className = 'status-badge ok';
// Update subtitle
const subtitle = document.getElementById('cronSubtitle');
const nextJob = jobs.find(j => !j.done);
subtitle.textContent = nextJob
? `Următorul: ${nextJob.time} ${nextJob.name}`
: 'Toate job-urile au rulat azi';
// Update details
const details = document.getElementById('cronDetails');
details.innerHTML = jobs.map(job => `
<div class="cron-item ${job.done ? 'done' : 'pending'}">
<i data-lucide="${job.done ? 'check-circle' : 'clock'}" class="cron-icon ${job.done ? 'done' : 'pending'}"></i>
<span class="cron-time">${job.time}</span>
<span class="cron-name">${job.name}</span>
</div>
`).join('');
lucide.createIcons();
}
function updateStatusSummary() {
const gitBadge = document.getElementById('gitBadge');
const anafBadge = document.getElementById('anafBadge');
const cronBadge = document.getElementById('cronBadge');
const summary = document.getElementById('statusSummary');
summary.textContent = `Git: ${gitBadge?.textContent || '-'} · ANAF: ${anafBadge?.textContent || '-'} · Cron: ${cronBadge?.textContent || '-'}`;
}
// Auto-refresh on page focus
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
loadStatus();
}
});
// Load data
async function loadIssues() {
try {
@@ -1323,6 +1640,7 @@
});
// Init
initStatusSections();
loadStatus();
loadIssues();
loadActivity();