Dashboard v2: remove old Kanban, add Status bar, collapsible sections

- Added Status bar from old kanban (ANAF, Git, Cron)
- All sections now collapsible (Status, Activity, Issues)
- Removed Kanban from navigation
- Removed kanban.html file
- Status bar collapsed by default
This commit is contained in:
Echo
2026-01-30 19:02:20 +00:00
parent b0d7bd0a08
commit e3f0708331
7 changed files with 559 additions and 1072 deletions

View File

@@ -31,11 +31,17 @@ python3 tools/email_send.py "dest@email.com" "Subiect" "Corp mesaj"
- **API:** `kanban/api.py` - **API:** `kanban/api.py`
- **Update task:** `python3 kanban/update_task.py` - **Update task:** `python3 kanban/update_task.py`
### YouTube Notes ### Notes (toate tipurile)
- **Folder:** `notes/youtube/` - **Folder:** `notes/` (subdirectoare: `youtube/`, `retete/`, etc.)
- **Update index:** `python3 tools/update_notes_index.py` - **Update index:** `python3 tools/update_notes_index.py`
- **Pagina web:** https://moltbot.tailf7372d.ts.net/echo/notes.html
- **Tags domeniu:** `@work`, `@health`, `@growth`, `@sprijin`, `@scout` - **Tags domeniu:** `@work`, `@health`, `@growth`, `@sprijin`, `@scout`
**IMPORTANT:** Când salvez orice notă (rețete, youtube, etc.), trebuie să:
1. Salvez în subdirectorul potrivit din `notes/`
2. Rulez `python3 tools/update_notes_index.py` pentru a actualiza indexul
3. Dau link-ul către pagina notes.html
### Git ### Git
- **Repo:** ~/clawd → gitea.romfast.ro/romfast/clawd - **Repo:** ~/clawd → gitea.romfast.ro/romfast/clawd
- **Commit script:** `python3 tools/git_commit.py --push` - **Commit script:** `python3 tools/git_commit.py --push`

View File

@@ -15,7 +15,7 @@
} }
.page-header { .page-header {
margin-bottom: var(--space-5); margin-bottom: var(--space-4);
} }
.page-title { .page-title {
@@ -30,11 +30,131 @@
color: var(--text-muted); color: var(--text-muted);
} }
/* Status bar */
.status-bar {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
overflow: hidden;
}
.status-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(22, 163, 74, 0.1));
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.status-header:hover {
filter: brightness(1.05);
}
.status-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: 600;
font-size: var(--text-sm);
color: var(--text-primary);
}
.status-title svg {
width: 16px;
height: 16px;
color: #22c55e;
}
.status-summary {
flex: 1;
font-size: var(--text-xs);
color: var(--text-muted);
text-align: right;
}
.status-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.status-bar.collapsed .status-toggle {
transform: rotate(-90deg);
}
.status-bar.collapsed .status-content {
display: none;
}
.status-content {
padding: var(--space-3) var(--space-4);
}
.status-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin-bottom: var(--space-2);
}
.status-item {
display: flex;
align-items: center;
gap: var(--space-1);
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 {
color: var(--text-muted);
}
.cron-list {
color: var(--text-secondary);
}
.cron-done {
color: var(--text-muted);
text-decoration: line-through;
}
/* Two-column dashboard */ /* Two-column dashboard */
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: var(--space-5); gap: var(--space-4);
} }
@media (max-width: 900px) { @media (max-width: 900px) {
@@ -57,6 +177,18 @@
justify-content: space-between; justify-content: space-between;
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.panel-header:hover {
filter: brightness(1.05);
}
.panel-header-left {
display: flex;
align-items: center;
gap: var(--space-2);
} }
.panel-title { .panel-title {
@@ -73,14 +205,37 @@
height: 18px; height: 18px;
} }
.panel-toggle {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.panel.collapsed .panel-toggle {
transform: rotate(-90deg);
}
.panel.collapsed .panel-body {
display: none;
}
.panel-actions { .panel-actions {
display: flex; display: flex;
gap: var(--space-2); gap: var(--space-2);
} }
.panel-count {
font-size: var(--text-xs);
color: var(--text-muted);
background: var(--bg-elevated);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.panel-body { .panel-body {
padding: var(--space-3); padding: var(--space-3);
max-height: 600px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
} }
@@ -189,11 +344,11 @@
/* Issues panel */ /* Issues panel */
.issues-panel .panel-header { .issues-panel .panel-header {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(22, 163, 74, 0.1)); background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(234, 88, 12, 0.1));
} }
.issues-panel .panel-title svg { .issues-panel .panel-title svg {
color: #22c55e; color: #f97316;
} }
.issues-filters { .issues-filters {
@@ -222,7 +377,7 @@
} }
.priority-group { .priority-group {
margin-bottom: var(--space-4); margin-bottom: var(--space-3);
} }
.priority-header { .priority-header {
@@ -234,6 +389,11 @@
color: var(--text-muted); color: var(--text-muted);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
cursor: pointer; cursor: pointer;
padding: var(--space-1) 0;
}
.priority-header:hover {
color: var(--text-secondary);
} }
.priority-header svg { .priority-header svg {
@@ -491,25 +651,6 @@
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
opacity: 0.5; opacity: 0.5;
} }
/* Old kanban link */
.legacy-link {
font-size: var(--text-xs);
color: var(--text-muted);
text-decoration: none;
display: flex;
align-items: center;
gap: var(--space-1);
}
.legacy-link:hover {
color: var(--accent);
}
.legacy-link svg {
width: 12px;
height: 12px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -523,10 +664,6 @@
<i data-lucide="layout-dashboard"></i> <i data-lucide="layout-dashboard"></i>
<span>Dashboard</span> <span>Dashboard</span>
</a> </a>
<a href="kanban.html" class="nav-item">
<i data-lucide="columns"></i>
<span>Kanban</span>
</a>
<a href="notes.html" class="nav-item"> <a href="notes.html" class="nav-item">
<i data-lucide="file-text"></i> <i data-lucide="file-text"></i>
<span>Notes</span> <span>Notes</span>
@@ -544,18 +681,55 @@
<main class="main"> <main class="main">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Dashboard</h1> <h1 class="page-title">Dashboard</h1>
<p class="page-subtitle" id="lastUpdated">Echo Work · Productivitate și proiecte</p> <p class="page-subtitle">Echo Work · Productivitate și proiecte</p>
</div>
<!-- Status Bar -->
<div class="status-bar" id="statusBar">
<div class="status-header" onclick="toggleSection('statusBar')">
<div class="status-title">
<i data-lucide="activity"></i>
<span>Status</span>
</div>
<div class="status-summary" id="statusSummary">Se încarcă...</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>
</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>
</div>
<div class="cron-row">
<span class="cron-label">Cron azi:</span>
<span class="cron-list" id="cronList">-</span>
</div>
</div>
</div> </div>
<div class="dashboard-grid"> <div class="dashboard-grid">
<!-- Activity Panel --> <!-- Activity Panel -->
<div class="panel activity-panel"> <div class="panel activity-panel" id="activityPanel">
<div class="panel-header"> <div class="panel-header" onclick="toggleSection('activityPanel')">
<div class="panel-title"> <div class="panel-header-left">
<i data-lucide="bot"></i> <i data-lucide="chevron-down" class="panel-toggle"></i>
<span>Clawdbot Activity</span> <div class="panel-title">
<i data-lucide="bot"></i>
<span>Activity</span>
</div>
<span class="panel-count" id="activityCount">0</span>
</div> </div>
<div class="panel-actions"> <div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary btn-sm" onclick="refreshActivity()" title="Refresh"> <button class="btn btn-secondary btn-sm" onclick="refreshActivity()" title="Refresh">
<i data-lucide="refresh-cw"></i> <i data-lucide="refresh-cw"></i>
</button> </button>
@@ -570,13 +744,17 @@
</div> </div>
<!-- Issues Panel --> <!-- Issues Panel -->
<div class="panel issues-panel"> <div class="panel issues-panel" id="issuesPanel">
<div class="panel-header"> <div class="panel-header" onclick="toggleSection('issuesPanel')">
<div class="panel-title"> <div class="panel-header-left">
<i data-lucide="check-square"></i> <i data-lucide="chevron-down" class="panel-toggle"></i>
<span>Issues</span> <div class="panel-title">
<i data-lucide="check-square"></i>
<span>Issues</span>
</div>
<span class="panel-count" id="issuesCount">0</span>
</div> </div>
<div class="panel-actions"> <div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-primary btn-sm" onclick="showAddModal()"> <button class="btn btn-primary btn-sm" onclick="showAddModal()">
<i data-lucide="plus"></i> <i data-lucide="plus"></i>
<span>Nou</span> <span>Nou</span>
@@ -676,6 +854,43 @@
initTheme(); initTheme();
lucide.createIcons(); lucide.createIcons();
// Collapsible sections
function getCollapsedSections() {
try {
return JSON.parse(localStorage.getItem('collapsedSections') || '["statusBar"]');
} catch { return ['statusBar']; }
}
function setCollapsedSections(sections) {
localStorage.setItem('collapsedSections', JSON.stringify(sections));
}
function initCollapsedSections() {
const collapsed = getCollapsedSections();
collapsed.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('collapsed');
});
}
function toggleSection(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('collapsed');
const collapsed = getCollapsedSections();
const idx = collapsed.indexOf(id);
if (el.classList.contains('collapsed')) {
if (idx === -1) collapsed.push(id);
} else {
if (idx > -1) collapsed.splice(idx, 1);
}
setCollapsedSections(collapsed);
}
initCollapsedSections();
// Data // Data
let issuesData = null; let issuesData = null;
let activityData = []; let activityData = [];
@@ -692,6 +907,64 @@
const priorityOrder = ['urgent-important', 'important', 'urgent', 'backlog']; const priorityOrder = ['urgent-important', 'important', 'urgent', 'backlog'];
// Status
async function loadStatus() {
try {
const response = await fetch('status.json?' + Date.now());
if (response.ok) {
const status = await response.json();
updateStatus(status);
}
} catch (e) {
console.log('No status.json');
}
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() {
const now = new Date();
const hour = now.getHours();
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 }
];
const listEl = document.getElementById('cronList');
listEl.innerHTML = jobs.map(job =>
`<span class="${job.done ? 'cron-done' : ''}">${job.time} ${job.name}</span>`
).join(' · ');
}
// Load data // Load data
async function loadIssues() { async function loadIssues() {
try { try {
@@ -699,6 +972,7 @@
issuesData = await response.json(); issuesData = await response.json();
populateProgramSelect(); populateProgramSelect();
renderIssues(); renderIssues();
updateIssuesCount();
} catch (error) { } catch (error) {
console.error('Error loading issues:', error); console.error('Error loading issues:', error);
document.getElementById('issuesBody').innerHTML = ` document.getElementById('issuesBody').innerHTML = `
@@ -711,14 +985,21 @@
} }
} }
function updateIssuesCount() {
if (!issuesData) return;
const todoCount = issuesData.issues.filter(i => i.status !== 'done').length;
document.getElementById('issuesCount').textContent = todoCount;
}
async function loadActivity() { async function loadActivity() {
// For now, show static data. Later we can fetch from API. // For now, show static data. TODO: fetch from API
activityData = [ activityData = [
{ type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' }, { type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' },
{ type: 'done', text: 'Salut', agent: 'Echo Work', time: '12:55' }, { type: 'done', text: 'Propunere dashboard v2', agent: 'Echo Work', time: '15:23' },
{ type: 'done', text: 'Identificare agent', agent: 'Echo Work', time: '12:48' } { type: 'done', text: 'Fix notes.html loading', agent: 'Echo Work', time: '17:39' }
]; ];
renderActivity(); renderActivity();
document.getElementById('activityCount').textContent = activityData.length;
} }
function refreshActivity() { function refreshActivity() {
@@ -892,12 +1173,12 @@
} }
renderIssues(); renderIssues();
updateIssuesCount();
await saveIssues(); await saveIssues();
showToast(issue.status === 'done' ? 'Issue finalizat! ✓' : 'Issue redeschis'); showToast(issue.status === 'done' ? 'Issue finalizat! ✓' : 'Issue redeschis');
} }
function editIssue(id) { function editIssue(id) {
// TODO: implement edit modal
const issue = issuesData.issues.find(i => i.id === id); const issue = issuesData.issues.find(i => i.id === id);
if (issue) { if (issue) {
alert(`Edit: ${issue.title}\n\n${issue.description || 'Fără descriere'}`); alert(`Edit: ${issue.title}\n\n${issue.description || 'Fără descriere'}`);
@@ -922,7 +1203,6 @@
function hideAddModal() { function hideAddModal() {
document.getElementById('addModal').classList.remove('active'); document.getElementById('addModal').classList.remove('active');
// Clear form
document.getElementById('issueTitle').value = ''; document.getElementById('issueTitle').value = '';
document.getElementById('issueDesc').value = ''; document.getElementById('issueDesc').value = '';
document.getElementById('issueProgram').value = ''; document.getElementById('issueProgram').value = '';
@@ -963,6 +1243,7 @@
issuesData.issues.unshift(newIssue); issuesData.issues.unshift(newIssue);
hideAddModal(); hideAddModal();
renderIssues(); renderIssues();
updateIssuesCount();
await saveIssues(); await saveIssues();
showToast('Issue adăugat!'); showToast('Issue adăugat!');
} }
@@ -997,6 +1278,7 @@
}); });
// Init // Init
loadStatus();
loadIssues(); loadIssues();
loadActivity(); loadActivity();
</script> </script>

File diff suppressed because it is too large Load Diff

1
kanban/notes-data Symbolic link
View File

@@ -0,0 +1 @@
../notes

View File

@@ -482,7 +482,7 @@
const notesCache = {}; const notesCache = {};
let notesIndex = []; let notesIndex = [];
const notesBasePath = "youtube-notes/"; const notesBasePath = "notes-data/";
const indexPath = notesBasePath + "index.json"; const indexPath = notesBasePath + "index.json";
// Load notes index from JSON // Load notes index from JSON

179
notes/index.json Normal file
View File

@@ -0,0 +1,179 @@
{
"notes": [
{
"file": "youtube/2026-01-30_clawdbot-personal-os-kitze.md",
"title": "How I Use Clawdbot to Run My Business and Life 24/7",
"date": "2026-01-30",
"tags": [
"clawdbot",
"productivity",
"personas",
"automation"
],
"domains": [
"work",
"growth"
],
"video": "https://youtu.be/YRhGtHfs1Lw",
"tldr": "Kitze folosește **UN SINGUR gateway Clawdbot** cu **MULTIPLE PERSONAS** pe Telegram/Discord. Fiecare personă are:\n- Personalitate diferită (avatar, stil de vorbit)\n- Skills diferite (acces la tool-uri...",
"category": "youtube"
},
{
"file": "youtube/2026-01-29_remotion-skill-claude-code.md",
"title": "How people are generating videos with Claude Code (Remotion Skill)",
"date": "2026-01-29",
"tags": [
"remotion",
"claude-code",
"video",
"automation"
],
"domains": [
"work"
],
"video": "https://youtu.be/7OR-L0AySn8",
"tldr": "Remotion Skill permite generarea de videouri programatic cu Claude Code. Funcționează prin React components → video export. Demo live: Claude creează animații YouTube (like, subscribe, cursor) doar di...",
"category": "youtube"
},
{
"file": "youtube/2026-01-29_gsd-framework-claude-code.md",
"title": "Forget Ralph Loops: The New GSD Framework for Claude Code",
"date": "2026-01-29",
"tags": [
"claude-code",
"gsd",
"framework",
"sub-agents",
"automation"
],
"domains": [
"work"
],
"video": "https://www.youtube.com/watch?v=l94A53kIUB0",
"tldr": "GSD (Get Shit Done) este un framework open-source pentru Claude Code care orchestrează sub-agenți pentru a completa proiecte urmând spec-driven development. Rezolvă problema \"context bloat\" prin rular...",
"category": "youtube"
},
{
"file": "youtube/2026-01-29_greseli-post-apa.md",
"title": "Greșeli frecvente în timpul postului doar cu apă",
"date": "2026-01-29",
"tags": [
"post",
"water-fasting",
"sănătate",
"detox"
],
"domains": [
"health"
],
"video": "https://youtu.be/4QjkI0sf64M",
"tldr": "Greșelile frecvente pe care le fac oamenii când țin post terapeutic cu apă și cum să le eviți. Puncte cheie: pregătire corectă, curățarea colonului, calitatea apei, și importanța scopului spiritual.",
"category": "youtube"
},
{
"file": "youtube/2026-01-29_cloudflare-tunnel-localhost-public.md",
"title": "Cloudflare Tunnel: Make Localhost Public Without Port Forwarding",
"date": "2026-01-29",
"tags": [
"cloudflare",
"tunnel",
"localhost",
"networking",
"devops"
],
"domains": [
"work"
],
"video": "https://youtu.be/etluT8UC-nw",
"tldr": "Cloudflare Tunnel permite expunerea unui server local (localhost) pe internet printr-un domeniu public, fără port forwarding, fără configurare router, fără expunerea IP-ului public. App-ul rămâne pe m...",
"category": "youtube"
},
{
"file": "youtube/2026-01-29_clawdbot-security-vulnerabilities.md",
"title": "It Got Worse (Clawdbot) - Security Vulnerabilities",
"date": "2026-01-29",
"tags": [
"clawdbot",
"security",
"vulnerabilities",
"hacking"
],
"domains": [
"work"
],
"video": "https://youtu.be/rPAKq2oQVBs",
"tldr": "Video critic despre vulnerabilitățile de securitate ale Clawdbot - sute/mii de instanțe au fost compromise. Probleme principale: porturi default, parole lipsă, reverse proxy misconfigurat, skills mali...",
"category": "youtube"
},
{
"file": "youtube/2025-01-30_clawdbot-5-use-cases.md",
"title": "5 Insane ClawdBot Use Cases You Need To Do Immediately",
"date": "2025-01-30",
"tags": [
"clawdbot",
"automation",
"productivity",
"ai-assistant"
],
"domains": [
"work"
],
"video": "https://www.youtube.com/watch?v=b-l9sGh1-UY",
"tldr": "5 use case-uri pentru ClawdBot care îl transformă dintr-un simplu chatbot într-un asistent proactiv care lucrează pentru tine chiar și când dormi.",
"category": "youtube"
},
{
"file": "youtube/2025-01-30_claude-code-do-work-pattern.md",
"title": "The Most Powerful Claude Code Pattern I've Found",
"date": "2025-01-30",
"tags": [
"claude-code",
"skills",
"workflow",
"automation",
"do-work"
],
"domains": [
"work"
],
"video": "https://youtu.be/I9-tdhxiH7w",
"tldr": "Un pattern puternic pentru Claude Code: **Do Work** - o coadă de task-uri pe care Claude le procesează automat, unul câte unul, în sub-agenți cu context curat. Ideea cheie: **construiește tool-uri pen...",
"category": "youtube"
},
{
"file": "retete/ciorba-burta-falsa-cu-pui.md",
"title": "Ciorbă de Burtă Falsă cu Pui și Ciuperci Pleurotus",
"date": "",
"tags": [],
"domains": [],
"video": "",
"tldr": "",
"category": "retete"
}
],
"stats": {
"total": 9,
"by_domain": {
"work": 7,
"health": 1,
"growth": 1,
"sprijin": 0,
"scout": 0
},
"by_category": {
"youtube": 8,
"retete": 1
}
},
"domains": [
"work",
"health",
"growth",
"sprijin",
"scout"
],
"categories": [
"youtube",
"retete"
]
}

View File

@@ -2,6 +2,7 @@
""" """
Generează index.json pentru notes din fișierele .md Generează index.json pentru notes din fișierele .md
Extrage titlu, dată, tags, și domenii (@work, @health, etc.) Extrage titlu, dată, tags, și domenii (@work, @health, etc.)
Scanează TOATE subdirectoarele din notes/ (youtube, retete, etc.)
""" """
import os import os
@@ -9,8 +10,11 @@ import re
import json import json
from pathlib import Path from pathlib import Path
NOTES_DIR = Path(__file__).parent.parent / "notes" / "youtube" NOTES_ROOT = Path(__file__).parent.parent / "notes"
INDEX_FILE = NOTES_DIR / "index.json" INDEX_FILE = NOTES_ROOT / "index.json"
# Subdirectoare de scanat (adaugă altele aici)
SCAN_DIRS = ['youtube', 'retete']
# Domenii de agenți # Domenii de agenți
VALID_DOMAINS = ['work', 'health', 'growth', 'sprijin', 'scout'] VALID_DOMAINS = ['work', 'health', 'growth', 'sprijin', 'scout']
@@ -66,27 +70,43 @@ def extract_metadata(filepath):
} }
def generate_index(): def generate_index():
"""Generează index.json din toate fișierele .md""" """Generează index.json din toate fișierele .md din toate subdirectoarele"""
notes = [] notes = []
# Stats per domeniu # Stats per domeniu
domain_stats = {d: 0 for d in VALID_DOMAINS} domain_stats = {d: 0 for d in VALID_DOMAINS}
# Stats per categorie
category_stats = {}
for filepath in sorted(NOTES_DIR.glob("*.md"), reverse=True): for subdir in SCAN_DIRS:
if filepath.name == 'index.json': notes_dir = NOTES_ROOT / subdir
if not notes_dir.exists():
print(f" (skipping {subdir}/ - not found)")
continue continue
try:
metadata = extract_metadata(filepath)
notes.append(metadata)
# Update domain stats print(f"Scanning notes/{subdir}/...")
for d in metadata['domains']: category_stats[subdir] = 0
domain_stats[d] += 1
domains_str = ' '.join([f'@{d}' for d in metadata['domains']]) if metadata['domains'] else '(no domain)' for filepath in sorted(notes_dir.glob("*.md"), reverse=True):
print(f" + {metadata['title'][:40]}... {domains_str}") if filepath.name == 'index.json':
except Exception as e: continue
print(f" ! Error processing {filepath.name}: {e}") try:
metadata = extract_metadata(filepath)
# Adaugă categoria (subdirectorul)
metadata['category'] = subdir
# Modifică path-ul fișierului să includă subdirectorul
metadata['file'] = f"{subdir}/{filepath.name}"
notes.append(metadata)
# Update stats
category_stats[subdir] += 1
for d in metadata['domains']:
domain_stats[d] += 1
domains_str = ' '.join([f'@{d}' for d in metadata['domains']]) if metadata['domains'] else ''
print(f" + {metadata['title'][:40]}... {domains_str}")
except Exception as e:
print(f" ! Error processing {filepath.name}: {e}")
# Sortează după dată descrescător # Sortează după dată descrescător
notes.sort(key=lambda x: x['date'], reverse=True) notes.sort(key=lambda x: x['date'], reverse=True)
@@ -96,9 +116,11 @@ def generate_index():
"notes": notes, "notes": notes,
"stats": { "stats": {
"total": len(notes), "total": len(notes),
"by_domain": domain_stats "by_domain": domain_stats,
"by_category": category_stats
}, },
"domains": VALID_DOMAINS "domains": VALID_DOMAINS,
"categories": SCAN_DIRS
} }
with open(INDEX_FILE, 'w', encoding='utf-8') as f: with open(INDEX_FILE, 'w', encoding='utf-8') as f:
@@ -106,8 +128,8 @@ def generate_index():
print(f"\n✅ Generated {INDEX_FILE} with {len(notes)} notes") print(f"\n✅ Generated {INDEX_FILE} with {len(notes)} notes")
print(f" Domains: {domain_stats}") print(f" Domains: {domain_stats}")
print(f" Categories: {category_stats}")
return output return output
if __name__ == "__main__": if __name__ == "__main__":
print("Scanning notes/youtube/...")
generate_index() generate_index()