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:
10
TOOLS.md
10
TOOLS.md
@@ -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`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1003
kanban/kanban.html
1003
kanban/kanban.html
File diff suppressed because it is too large
Load Diff
1
kanban/notes-data
Symbolic link
1
kanban/notes-data
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../notes
|
||||||
@@ -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
179
notes/index.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user