Restructurare completă: kanban→dashboard, notes→kb, ANAF→tools/
- Mutare și reorganizare foldere proiecte - Actualizare path-uri în TOOLS.md - Sincronizare configurații agenți - 79 fișiere actualizate
This commit is contained in:
450
dashboard/api.py
Normal file
450
dashboard/api.py
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple API server for Echo Task Board.
|
||||
Handles YouTube summarization requests.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent
|
||||
TOOLS_DIR = BASE_DIR / 'tools'
|
||||
NOTES_DIR = BASE_DIR / 'kb' / 'youtube'
|
||||
KANBAN_DIR = BASE_DIR / 'dashboard'
|
||||
|
||||
class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == '/api/youtube':
|
||||
self.handle_youtube()
|
||||
elif self.path == '/api/files':
|
||||
self.handle_files_post()
|
||||
elif self.path == '/api/refresh-index':
|
||||
self.handle_refresh_index()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def handle_refresh_index(self):
|
||||
"""Regenerate kb/index.json"""
|
||||
try:
|
||||
script = TOOLS_DIR / 'update_notes_index.py'
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Parse output for stats
|
||||
output = result.stdout
|
||||
total_match = re.search(r'with (\d+) notes', output)
|
||||
total = int(total_match.group(1)) if total_match else 0
|
||||
|
||||
self.send_json({
|
||||
'success': True,
|
||||
'message': f'Index regenerat cu {total} notițe',
|
||||
'total': total,
|
||||
'output': output
|
||||
})
|
||||
else:
|
||||
self.send_json({
|
||||
'success': False,
|
||||
'error': result.stderr or 'Unknown error'
|
||||
}, 500)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({'success': False, 'error': 'Timeout'}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def handle_files_post(self):
|
||||
"""Save file content."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
path = data.get('path', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
workspace = Path('/home/moltbot/clawd')
|
||||
target = (workspace / path).resolve()
|
||||
|
||||
if not str(target).startswith(str(workspace)):
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
|
||||
# Create parent dirs if needed
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write file
|
||||
target.write_text(content, encoding='utf-8')
|
||||
|
||||
self.send_json({
|
||||
'status': 'saved',
|
||||
'path': path,
|
||||
'size': len(content)
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/api/status':
|
||||
self.send_json({'status': 'ok', 'time': datetime.now().isoformat()})
|
||||
elif self.path == '/api/git' or self.path.startswith('/api/git?'):
|
||||
self.handle_git_status()
|
||||
elif self.path == '/api/agents' or self.path.startswith('/api/agents?'):
|
||||
self.handle_agents_status()
|
||||
elif self.path.startswith('/api/files'):
|
||||
self.handle_files_get()
|
||||
elif self.path.startswith('/api/'):
|
||||
self.send_error(404)
|
||||
else:
|
||||
# Serve static files
|
||||
super().do_GET()
|
||||
|
||||
def handle_git_status(self):
|
||||
"""Get git status for dashboard."""
|
||||
try:
|
||||
workspace = Path('/home/moltbot/clawd')
|
||||
|
||||
# Get current branch
|
||||
branch = subprocess.run(
|
||||
['git', 'branch', '--show-current'],
|
||||
cwd=workspace, capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
|
||||
# Get last commit
|
||||
last_commit = subprocess.run(
|
||||
['git', 'log', '-1', '--format=%h|%s|%cr'],
|
||||
cwd=workspace, capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
|
||||
commit_parts = last_commit.split('|') if last_commit else ['', '', '']
|
||||
|
||||
# Get uncommitted files
|
||||
status_output = subprocess.run(
|
||||
['git', 'status', '--short'],
|
||||
cwd=workspace, capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
|
||||
uncommitted = status_output.split('\n') if status_output else []
|
||||
uncommitted = [f for f in uncommitted if f.strip()]
|
||||
|
||||
# Get diff stats if there are uncommitted files
|
||||
diff_stat = ''
|
||||
if uncommitted:
|
||||
diff_stat = subprocess.run(
|
||||
['git', 'diff', '--stat', '--cached'],
|
||||
cwd=workspace, capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
if not diff_stat:
|
||||
diff_stat = subprocess.run(
|
||||
['git', 'diff', '--stat'],
|
||||
cwd=workspace, capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
|
||||
self.send_json({
|
||||
'branch': branch,
|
||||
'lastCommit': {
|
||||
'hash': commit_parts[0] if len(commit_parts) > 0 else '',
|
||||
'message': commit_parts[1] if len(commit_parts) > 1 else '',
|
||||
'time': commit_parts[2] if len(commit_parts) > 2 else ''
|
||||
},
|
||||
'uncommitted': uncommitted,
|
||||
'uncommittedCount': len(uncommitted),
|
||||
'diffStat': diff_stat,
|
||||
'clean': len(uncommitted) == 0
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_agents_status(self):
|
||||
"""Get agents status - fast version reading session files directly."""
|
||||
try:
|
||||
# Define known agents
|
||||
agents_config = [
|
||||
{'id': 'echo', 'name': 'Echo', 'emoji': '🌀'},
|
||||
{'id': 'echo-work', 'name': 'Work', 'emoji': '⚡'},
|
||||
{'id': 'echo-health', 'name': 'Health', 'emoji': '❤️'},
|
||||
{'id': 'echo-growth', 'name': 'Growth', 'emoji': '🪜'},
|
||||
{'id': 'echo-sprijin', 'name': 'Sprijin', 'emoji': '⭕'},
|
||||
{'id': 'echo-scout', 'name': 'Scout', 'emoji': '⚜️'},
|
||||
]
|
||||
|
||||
# Check active sessions by reading session files directly (fast)
|
||||
active_agents = set()
|
||||
sessions_base = Path.home() / '.clawdbot' / 'agents'
|
||||
|
||||
if sessions_base.exists():
|
||||
for agent_dir in sessions_base.iterdir():
|
||||
if agent_dir.is_dir():
|
||||
sessions_file = agent_dir / 'sessions' / 'sessions.json'
|
||||
if sessions_file.exists():
|
||||
try:
|
||||
data = json.loads(sessions_file.read_text())
|
||||
# sessions.json is an object with session keys
|
||||
now = datetime.now().timestamp() * 1000
|
||||
for key, sess in data.items():
|
||||
if isinstance(sess, dict):
|
||||
last_active = sess.get('updatedAt', 0)
|
||||
if now - last_active < 30 * 60 * 1000: # 30 min
|
||||
active_agents.add(agent_dir.name)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Build response
|
||||
agents = []
|
||||
for cfg in agents_config:
|
||||
agents.append({
|
||||
'id': cfg['id'],
|
||||
'name': cfg['name'],
|
||||
'emoji': cfg['emoji'],
|
||||
'active': cfg['id'] in active_agents
|
||||
})
|
||||
|
||||
self.send_json({'agents': agents})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_files_get(self):
|
||||
"""List files or get file content."""
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
parsed = urlparse(self.path)
|
||||
params = parse_qs(parsed.query)
|
||||
|
||||
path = params.get('path', [''])[0]
|
||||
action = params.get('action', ['list'])[0]
|
||||
|
||||
# Security: only allow access within workspace
|
||||
workspace = Path('/home/moltbot/clawd')
|
||||
try:
|
||||
target = (workspace / path).resolve()
|
||||
if not str(target).startswith(str(workspace)):
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
except:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
|
||||
if action == 'list':
|
||||
if not target.exists():
|
||||
self.send_json({'error': 'Path not found'}, 404)
|
||||
return
|
||||
|
||||
if target.is_file():
|
||||
# Return file content
|
||||
try:
|
||||
content = target.read_text(encoding='utf-8', errors='replace')
|
||||
self.send_json({
|
||||
'type': 'file',
|
||||
'path': path,
|
||||
'name': target.name,
|
||||
'content': content[:100000], # Limit to 100KB
|
||||
'size': target.stat().st_size,
|
||||
'truncated': target.stat().st_size > 100000
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
else:
|
||||
# List directory
|
||||
items = []
|
||||
try:
|
||||
for item in sorted(target.iterdir()):
|
||||
if item.name.startswith('.'):
|
||||
continue
|
||||
items.append({
|
||||
'name': item.name,
|
||||
'type': 'dir' if item.is_dir() else 'file',
|
||||
'size': item.stat().st_size if item.is_file() else None,
|
||||
'path': str(item.relative_to(workspace))
|
||||
})
|
||||
self.send_json({
|
||||
'type': 'dir',
|
||||
'path': path,
|
||||
'items': items
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
else:
|
||||
self.send_json({'error': 'Unknown action'}, 400)
|
||||
|
||||
def handle_youtube(self):
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
url = data.get('url', '').strip()
|
||||
|
||||
if not url or 'youtube.com' not in url and 'youtu.be' not in url:
|
||||
self.send_json({'error': 'URL YouTube invalid'}, 400)
|
||||
return
|
||||
|
||||
# Process synchronously (simpler, avoids fork issues)
|
||||
try:
|
||||
print(f"Processing YouTube URL: {url}")
|
||||
result = process_youtube(url)
|
||||
print(f"Processing result: {result}")
|
||||
self.send_json({
|
||||
'status': 'done',
|
||||
'message': 'Notița a fost creată! Refresh pagina Notes.'
|
||||
})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"YouTube processing error: {e}")
|
||||
traceback.print_exc()
|
||||
self.send_json({
|
||||
'status': 'error',
|
||||
'message': f'Eroare: {str(e)}'
|
||||
}, 500)
|
||||
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def process_youtube(url):
|
||||
"""Download subtitles, summarize, save note."""
|
||||
import time
|
||||
|
||||
# Get video info and subtitles
|
||||
yt_dlp = os.path.expanduser('~/.local/bin/yt-dlp')
|
||||
|
||||
# Get title
|
||||
result = subprocess.run(
|
||||
[yt_dlp, '--dump-json', '--no-download', url],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to get video info: {result.stderr}")
|
||||
return
|
||||
|
||||
info = json.loads(result.stdout)
|
||||
title = info.get('title', 'Unknown')
|
||||
duration = info.get('duration', 0)
|
||||
video_id = info.get('id', 'unknown')
|
||||
|
||||
# Download subtitles
|
||||
temp_dir = Path('/tmp/yt_subs')
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
for f in temp_dir.glob('*'):
|
||||
f.unlink()
|
||||
|
||||
subprocess.run([
|
||||
yt_dlp, '--write-auto-subs', '--sub-langs', 'en',
|
||||
'--skip-download', '--sub-format', 'vtt',
|
||||
'-o', str(temp_dir / '%(id)s'),
|
||||
url
|
||||
], capture_output=True, timeout=120)
|
||||
|
||||
# Find and read subtitle file
|
||||
transcript = None
|
||||
for sub_file in temp_dir.glob('*.vtt'):
|
||||
content = sub_file.read_text(encoding='utf-8', errors='replace')
|
||||
transcript = clean_vtt(content)
|
||||
break
|
||||
|
||||
if not transcript:
|
||||
print("No subtitles found")
|
||||
return
|
||||
|
||||
# Create note filename
|
||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
||||
slug = re.sub(r'[^\w\s-]', '', title.lower())[:50].strip().replace(' ', '-')
|
||||
filename = f"{date_str}_{slug}.md"
|
||||
|
||||
# Create simple note (without AI summary for now - just transcript)
|
||||
note_content = f"""# {title}
|
||||
|
||||
**Video:** {url}
|
||||
**Duration:** {duration // 60}:{duration % 60:02d}
|
||||
**Saved:** {date_str}
|
||||
**Tags:** #youtube #to-summarize
|
||||
|
||||
---
|
||||
|
||||
## Transcript
|
||||
|
||||
{transcript[:15000]}
|
||||
|
||||
---
|
||||
|
||||
*Notă: Sumarizarea va fi adăugată de Echo.*
|
||||
"""
|
||||
|
||||
# Save note
|
||||
NOTES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
note_path = NOTES_DIR / filename
|
||||
note_path.write_text(note_content, encoding='utf-8')
|
||||
|
||||
# Update index
|
||||
subprocess.run([
|
||||
sys.executable, str(TOOLS_DIR / 'update_notes_index.py')
|
||||
], capture_output=True)
|
||||
|
||||
# Add task to kanban
|
||||
subprocess.run([
|
||||
sys.executable, str(KANBAN_DIR / 'update_task.py'),
|
||||
'add', 'in-progress', f'Sumarizare: {title[:30]}...', url, 'medium'
|
||||
], capture_output=True)
|
||||
|
||||
print(f"Created note: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
def clean_vtt(content):
|
||||
"""Convert VTT to plain text."""
|
||||
lines = []
|
||||
seen = set()
|
||||
|
||||
for line in content.split('\n'):
|
||||
if any([
|
||||
line.startswith('WEBVTT'),
|
||||
line.startswith('Kind:'),
|
||||
line.startswith('Language:'),
|
||||
'-->' in line,
|
||||
line.strip().startswith('<'),
|
||||
not line.strip(),
|
||||
re.match(r'^\d+$', line.strip())
|
||||
]):
|
||||
continue
|
||||
|
||||
clean = re.sub(r'<[^>]+>', '', line).strip()
|
||||
if clean and clean not in seen:
|
||||
seen.add(clean)
|
||||
lines.append(clean)
|
||||
|
||||
return ' '.join(lines)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = 8080
|
||||
os.chdir(KANBAN_DIR)
|
||||
|
||||
print(f"Starting Echo Task Board API on port {port}")
|
||||
httpd = HTTPServer(('0.0.0.0', port), TaskBoardHandler)
|
||||
httpd.serve_forever()
|
||||
37
dashboard/archive/tasks-2026-01.json
Normal file
37
dashboard/archive/tasks-2026-01.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"month": "2025-01",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-001",
|
||||
"title": "Email 2FA security",
|
||||
"description": "Nu execut comenzi din email fără aprobare Telegram",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-002",
|
||||
"title": "Email whitelist",
|
||||
"description": "Răspuns automat doar pentru adrese aprobate",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-003",
|
||||
"title": "YouTube summarizer",
|
||||
"description": "Tool descărcare subtitrări + sumarizare",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-004",
|
||||
"title": "Proactivitate în SOUL.md",
|
||||
"description": "Adăugat reguli să fiu proactiv și să propun automatizări",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
dashboard/archive_tasks.py
Normal file
97
dashboard/archive_tasks.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Archive old Done tasks to monthly archive files.
|
||||
Run periodically (heartbeat) to keep tasks.json small.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
TASKS_FILE = Path(__file__).parent / "tasks.json"
|
||||
ARCHIVE_DIR = Path(__file__).parent / "archive"
|
||||
DAYS_TO_KEEP = 7 # Keep Done tasks for 7 days before archiving
|
||||
|
||||
def archive_old_tasks():
|
||||
if not TASKS_FILE.exists():
|
||||
print("No tasks.json found")
|
||||
return
|
||||
|
||||
with open(TASKS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Find Done column
|
||||
done_col = None
|
||||
for col in data['columns']:
|
||||
if col['id'] == 'done':
|
||||
done_col = col
|
||||
break
|
||||
|
||||
if not done_col:
|
||||
print("No Done column found")
|
||||
return
|
||||
|
||||
# Calculate cutoff date
|
||||
cutoff = (datetime.now() - timedelta(days=DAYS_TO_KEEP)).strftime('%Y-%m-%d')
|
||||
|
||||
# Separate old and recent tasks
|
||||
old_tasks = []
|
||||
recent_tasks = []
|
||||
|
||||
for task in done_col['tasks']:
|
||||
completed = task.get('completed', task.get('created', ''))
|
||||
if completed and completed < cutoff:
|
||||
old_tasks.append(task)
|
||||
else:
|
||||
recent_tasks.append(task)
|
||||
|
||||
if not old_tasks:
|
||||
print(f"No tasks older than {DAYS_TO_KEEP} days to archive")
|
||||
return
|
||||
|
||||
# Create archive directory
|
||||
ARCHIVE_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Group old tasks by month
|
||||
by_month = {}
|
||||
for task in old_tasks:
|
||||
completed = task.get('completed', task.get('created', ''))[:7] # YYYY-MM
|
||||
if completed not in by_month:
|
||||
by_month[completed] = []
|
||||
by_month[completed].append(task)
|
||||
|
||||
# Write to monthly archive files
|
||||
for month, tasks in by_month.items():
|
||||
archive_file = ARCHIVE_DIR / f"tasks-{month}.json"
|
||||
|
||||
# Load existing archive
|
||||
if archive_file.exists():
|
||||
with open(archive_file, 'r') as f:
|
||||
archive = json.load(f)
|
||||
else:
|
||||
archive = {"month": month, "tasks": []}
|
||||
|
||||
# Add new tasks (avoid duplicates by ID)
|
||||
existing_ids = {t['id'] for t in archive['tasks']}
|
||||
for task in tasks:
|
||||
if task['id'] not in existing_ids:
|
||||
archive['tasks'].append(task)
|
||||
|
||||
# Save archive
|
||||
with open(archive_file, 'w') as f:
|
||||
json.dump(archive, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Archived {len(tasks)} tasks to {archive_file.name}")
|
||||
|
||||
# Update tasks.json with only recent Done tasks
|
||||
done_col['tasks'] = recent_tasks
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
|
||||
with open(TASKS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Kept {len(recent_tasks)} recent Done tasks, archived {len(old_tasks)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
archive_old_tasks()
|
||||
438
dashboard/common.css
Normal file
438
dashboard/common.css
Normal file
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* Echo Design System
|
||||
* Modern, minimalist, unified UI
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
CSS Variables - Design Tokens
|
||||
============================================ */
|
||||
:root {
|
||||
/* Colors - Dark theme (high contrast) */
|
||||
--bg-base: #13131a;
|
||||
--bg-surface: rgba(255, 255, 255, 0.08);
|
||||
--bg-surface-hover: rgba(255, 255, 255, 0.12);
|
||||
--bg-surface-active: rgba(255, 255, 255, 0.16);
|
||||
--bg-elevated: rgba(255, 255, 255, 0.10);
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #d4d4d4;
|
||||
--text-muted: #a0a0a0;
|
||||
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--accent-subtle: rgba(59, 130, 246, 0.2);
|
||||
|
||||
--border: rgba(255, 255, 255, 0.2);
|
||||
--border-focus: rgba(59, 130, 246, 0.7);
|
||||
|
||||
/* Header specific */
|
||||
--header-bg: rgba(19, 19, 26, 0.95);
|
||||
|
||||
--success: #22c55e;
|
||||
--warning: #eab308;
|
||||
--error: #ef4444;
|
||||
|
||||
/* Spacing (8px grid) */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-base: 0.2s ease;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
[data-theme="light"] {
|
||||
--bg-base: #f8f9fa;
|
||||
--bg-surface: rgba(0, 0, 0, 0.04);
|
||||
--bg-surface-hover: rgba(0, 0, 0, 0.08);
|
||||
--bg-surface-active: rgba(0, 0, 0, 0.12);
|
||||
--bg-elevated: rgba(0, 0, 0, 0.06);
|
||||
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #444444;
|
||||
--text-muted: #666666;
|
||||
|
||||
--border: rgba(0, 0, 0, 0.12);
|
||||
--border-focus: rgba(59, 130, 246, 0.5);
|
||||
|
||||
--accent-subtle: rgba(59, 130, 246, 0.12);
|
||||
|
||||
/* Header light */
|
||||
--header-bg: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Reset & Base
|
||||
============================================ */
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-base);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Header / Navigation
|
||||
============================================ */
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: var(--header-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--text-primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Theme toggle */
|
||||
.theme-toggle {
|
||||
padding: var(--space-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Cards
|
||||
============================================ */
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Buttons
|
||||
============================================ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Inputs
|
||||
============================================ */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Tags / Badges
|
||||
============================================ */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
padding: 2px 6px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-active);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Grid Layouts
|
||||
============================================ */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
|
||||
/* ============================================
|
||||
Status Colors
|
||||
============================================ */
|
||||
.status-success { color: var(--success); }
|
||||
.status-warning { color: var(--warning); }
|
||||
.status-error { color: var(--error); }
|
||||
|
||||
/* ============================================
|
||||
Scrollbar
|
||||
============================================ */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-surface-active);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive
|
||||
============================================ */
|
||||
@media (max-width: 768px) {
|
||||
/* Larger base font for mobile readability */
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.nav-item span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.grid-2, .grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Larger touch targets */
|
||||
.btn, .input, .tag {
|
||||
min-height: 44px;
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Utilities
|
||||
============================================ */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-2 { gap: var(--space-2); }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
1
dashboard/conversations
Symbolic link
1
dashboard/conversations
Symbolic link
@@ -0,0 +1 @@
|
||||
../conversations
|
||||
16
dashboard/echo-taskboard.service
Normal file
16
dashboard/echo-taskboard.service
Normal file
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Echo Task Board API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=moltbot
|
||||
WorkingDirectory=/home/moltbot/clawd/kanban
|
||||
ExecStart=/usr/bin/python3 /home/moltbot/clawd/kanban/api.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:/var/log/echo-taskboard.log
|
||||
StandardError=append:/var/log/echo-taskboard.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
707
dashboard/files.html
Normal file
707
dashboard/files.html
Normal file
@@ -0,0 +1,707 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Echo · Files</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="swipe-nav.js"></script>
|
||||
<style>
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 52px);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.toolbar {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.breadcrumb-item.current {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.breadcrumb-sep {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.breadcrumb-sep svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* View modes */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: var(--bg-base);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: var(--space-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.view-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Content area */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Browse panel */
|
||||
.browse-panel {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.browse-panel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.file-icon.folder svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Editor panel */
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.editor-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#codeEditor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: none;
|
||||
padding: var(--space-5);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#markdownPreview {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--space-5);
|
||||
overflow-y: auto;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
#markdownPreview h1, #markdownPreview h2, #markdownPreview h3 {
|
||||
color: var(--text-primary);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#markdownPreview h1 { font-size: 1.8em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
||||
#markdownPreview h2 { font-size: 1.4em; }
|
||||
#markdownPreview h3 { font-size: 1.2em; }
|
||||
|
||||
#markdownPreview p { margin-bottom: 1em; }
|
||||
#markdownPreview ul, #markdownPreview ol { margin-bottom: 1em; padding-left: 2em; }
|
||||
#markdownPreview li { margin-bottom: 0.3em; }
|
||||
#markdownPreview strong { color: var(--text-primary); }
|
||||
#markdownPreview hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
|
||||
#markdownPreview code { background: var(--bg-surface); padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
|
||||
#markdownPreview pre { background: var(--bg-surface); padding: 1em; border-radius: 8px; overflow-x: auto; }
|
||||
#markdownPreview blockquote { border-left: 3px solid var(--accent); padding-left: 1em; margin-left: 0; color: var(--text-muted); }
|
||||
|
||||
.preview-active #codeEditor { display: none; }
|
||||
.preview-active #markdownPreview { display: block; }
|
||||
|
||||
.btn-preview.active { background: var(--accent); color: white; }
|
||||
|
||||
.editor-footer {
|
||||
padding: var(--space-2) var(--space-5);
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-saved { color: var(--success); }
|
||||
.status-modified { color: var(--warning); }
|
||||
.status-error { color: var(--error); }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--text-muted);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toolbar {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<a href="index.html" class="logo">
|
||||
<i data-lucide="circle-dot"></i>
|
||||
Echo
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="index.html" class="nav-item">
|
||||
<i data-lucide="layout-list"></i>
|
||||
<span>Tasks</span>
|
||||
</a>
|
||||
<a href="notes.html" class="nav-item">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>KB</span>
|
||||
</a>
|
||||
<a href="files.html" class="nav-item active">
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="toolbar">
|
||||
<div class="breadcrumb" id="breadcrumb">
|
||||
<span class="breadcrumb-item current" onclick="loadPath('')">~/clawd</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" id="browseBtn" onclick="showBrowse()" title="Browse">
|
||||
<i data-lucide="layout-grid"></i>
|
||||
</button>
|
||||
<button class="view-btn" id="editorBtn" onclick="showEditor()" title="Editor">
|
||||
<i data-lucide="code"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="browse-panel" id="browsePanel">
|
||||
<div class="file-grid" id="fileGrid">
|
||||
<div class="empty-state">
|
||||
<i data-lucide="loader"></i>
|
||||
<p>Se încarcă...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-panel" id="editorPanel">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title">
|
||||
<i data-lucide="file"></i>
|
||||
<span id="editorFileName">Niciun fișier</span>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button class="btn btn-ghost" onclick="showBrowse()" title="Înapoi">
|
||||
<i data-lucide="arrow-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-preview" onclick="togglePreview()" id="previewBtn" style="display:none;" title="Preview Markdown">
|
||||
<i data-lucide="eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost" onclick="reloadFile()" id="reloadBtn" disabled title="Reload">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveFile()" id="saveBtn" disabled>
|
||||
<i data-lucide="save"></i>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-body" id="editorBody">
|
||||
<textarea id="codeEditor" placeholder="Selectează un fișier..."></textarea>
|
||||
<div id="markdownPreview"></div>
|
||||
</div>
|
||||
<div class="editor-footer">
|
||||
<span id="statusText">Ready</span>
|
||||
<span id="fileInfo"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Theme
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
updateThemeIcon(saved);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateThemeIcon(next);
|
||||
}
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
const icon = document.getElementById('themeIcon');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
initTheme();
|
||||
lucide.createIcons();
|
||||
|
||||
const API_BASE = window.location.pathname.includes('/echo/') ? '/echo' : '';
|
||||
let currentPath = '';
|
||||
let currentFile = null;
|
||||
let originalContent = '';
|
||||
let isModified = false;
|
||||
|
||||
function showBrowse() {
|
||||
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
|
||||
document.getElementById('browsePanel').classList.remove('hidden');
|
||||
document.getElementById('editorPanel').classList.remove('active');
|
||||
document.getElementById('browseBtn').classList.add('active');
|
||||
document.getElementById('editorBtn').classList.remove('active');
|
||||
}
|
||||
|
||||
function showEditor() {
|
||||
document.getElementById('browsePanel').classList.add('hidden');
|
||||
document.getElementById('editorPanel').classList.add('active');
|
||||
document.getElementById('browseBtn').classList.remove('active');
|
||||
document.getElementById('editorBtn').classList.add('active');
|
||||
}
|
||||
|
||||
async function loadPath(path = '') {
|
||||
currentPath = path;
|
||||
updateBreadcrumb();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/files?path=${encodeURIComponent(path)}&action=list`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
showError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'dir') {
|
||||
renderFileGrid(data.items);
|
||||
updateURL(path);
|
||||
} else if (data.type === 'file') {
|
||||
openFile(path, data);
|
||||
}
|
||||
} catch (e) {
|
||||
showError('Eroare: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBreadcrumb() {
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
const parts = currentPath.split('/').filter(p => p);
|
||||
|
||||
let html = `<span class="breadcrumb-item ${parts.length === 0 ? 'current' : ''}" onclick="loadPath('')">~/clawd</span>`;
|
||||
let buildPath = '';
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
buildPath += (buildPath ? '/' : '') + part;
|
||||
const p = buildPath;
|
||||
const isCurrent = i === parts.length - 1;
|
||||
html += `
|
||||
<span class="breadcrumb-sep"><i data-lucide="chevron-right"></i></span>
|
||||
<span class="breadcrumb-item ${isCurrent ? 'current' : ''}" onclick="loadPath('${p}')">${part}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
breadcrumb.innerHTML = html;
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderFileGrid(items) {
|
||||
const grid = document.getElementById('fileGrid');
|
||||
|
||||
if (items.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i data-lucide="folder-open"></i>
|
||||
<p>Folder gol</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
grid.innerHTML = items.map(item => `
|
||||
<div class="file-item ${currentFile === item.path ? 'active' : ''}" onclick="handleClick('${item.path}', '${item.type}')">
|
||||
<div class="file-icon ${item.type === 'dir' ? 'folder' : ''}">
|
||||
<i data-lucide="${item.type === 'dir' ? 'folder' : getFileIcon(item.name)}"></i>
|
||||
</div>
|
||||
<div class="file-name">${item.name}</div>
|
||||
${item.size ? `<div class="file-size">${formatSize(item.size)}</div>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function getFileIcon(name) {
|
||||
const ext = name.split('.').pop().toLowerCase();
|
||||
const icons = {
|
||||
'md': 'file-text',
|
||||
'txt': 'file-text',
|
||||
'json': 'file-json',
|
||||
'js': 'file-code',
|
||||
'py': 'file-code',
|
||||
'html': 'file-code',
|
||||
'css': 'file-code',
|
||||
'sh': 'terminal',
|
||||
'yml': 'file-cog',
|
||||
'yaml': 'file-cog',
|
||||
'log': 'file-text',
|
||||
'xsd': 'file-code',
|
||||
'pdf': 'file-text'
|
||||
};
|
||||
return icons[ext] || 'file';
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function handleClick(path, type) {
|
||||
if (type === 'dir') {
|
||||
loadPath(path);
|
||||
} else {
|
||||
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
|
||||
loadPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
function openFile(path, data) {
|
||||
currentFile = path;
|
||||
originalContent = data.content;
|
||||
updateURL(path);
|
||||
|
||||
document.getElementById('editorFileName').textContent = data.name;
|
||||
document.getElementById('codeEditor').value = data.content;
|
||||
document.getElementById('saveBtn').disabled = false;
|
||||
document.getElementById('reloadBtn').disabled = false;
|
||||
document.getElementById('fileInfo').textContent = formatSize(data.size);
|
||||
|
||||
// Show preview button for markdown files
|
||||
const isMarkdown = path.endsWith('.md');
|
||||
document.getElementById('previewBtn').style.display = isMarkdown ? 'flex' : 'none';
|
||||
|
||||
// Auto-activate preview for markdown files
|
||||
if (isMarkdown) {
|
||||
const preview = document.getElementById('markdownPreview');
|
||||
preview.innerHTML = marked.parse(data.content);
|
||||
document.getElementById('editorBody').classList.add('preview-active');
|
||||
document.getElementById('previewBtn').classList.add('active');
|
||||
} else {
|
||||
document.getElementById('editorBody').classList.remove('preview-active');
|
||||
document.getElementById('previewBtn').classList.remove('active');
|
||||
}
|
||||
|
||||
if (data.truncated) {
|
||||
setStatus('Fișier trunchiat', 'error');
|
||||
} else {
|
||||
setStatus('Loaded', 'saved');
|
||||
}
|
||||
|
||||
isModified = false;
|
||||
showEditor();
|
||||
}
|
||||
|
||||
function togglePreview() {
|
||||
const editorBody = document.getElementById('editorBody');
|
||||
const previewBtn = document.getElementById('previewBtn');
|
||||
const preview = document.getElementById('markdownPreview');
|
||||
const content = document.getElementById('codeEditor').value;
|
||||
|
||||
if (editorBody.classList.contains('preview-active')) {
|
||||
// Switch to edit mode
|
||||
editorBody.classList.remove('preview-active');
|
||||
previewBtn.classList.remove('active');
|
||||
setStatus('Edit mode', 'saved');
|
||||
} else {
|
||||
// Switch to preview mode
|
||||
preview.innerHTML = marked.parse(content);
|
||||
editorBody.classList.add('preview-active');
|
||||
previewBtn.classList.add('active');
|
||||
setStatus('Preview mode', 'saved');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if (!currentFile) return;
|
||||
|
||||
const content = document.getElementById('codeEditor').value;
|
||||
|
||||
try {
|
||||
document.getElementById('saveBtn').disabled = true;
|
||||
setStatus('Se salvează...', 'modified');
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: currentFile, content })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
setStatus('Eroare: ' + data.error, 'error');
|
||||
} else {
|
||||
originalContent = content;
|
||||
isModified = false;
|
||||
setStatus('Salvat ✓', 'saved');
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus('Eroare: ' + e.message, 'error');
|
||||
}
|
||||
|
||||
document.getElementById('saveBtn').disabled = false;
|
||||
}
|
||||
|
||||
function reloadFile() {
|
||||
if (currentFile && confirm('Renunți la modificări?')) {
|
||||
loadPath(currentFile);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, type) {
|
||||
const status = document.getElementById('statusText');
|
||||
status.textContent = text;
|
||||
status.className = 'status-' + type;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i data-lucide="alert-circle"></i>
|
||||
<p style="color: var(--error)">${msg}</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function updateURL(path) {
|
||||
if (path) {
|
||||
history.replaceState(null, '', `#${path}`);
|
||||
} else {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
function getPathFromURL() {
|
||||
const hash = window.location.hash;
|
||||
return hash ? decodeURIComponent(hash.slice(1)) : '';
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', () => loadPath(getPathFromURL()));
|
||||
|
||||
document.getElementById('codeEditor').addEventListener('input', function() {
|
||||
isModified = this.value !== originalContent;
|
||||
setStatus(isModified ? 'Modified' : 'Ready', isModified ? 'modified' : 'saved');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveFile();
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
loadPath(getPathFromURL());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
497
dashboard/grup-sprijin.html
Normal file
497
dashboard/grup-sprijin.html
Normal file
@@ -0,0 +1,497 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Echo · Grup Sprijin</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
.main {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-6);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.item-card.used {
|
||||
opacity: 0.7;
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.item-type {
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-type.exercitiu { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
||||
.item-type.meditatie { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; }
|
||||
.item-type.intrebare { background: rgba(20, 184, 166, 0.2); color: #14b8a6; }
|
||||
.item-type.reflectie { background: rgba(249, 115, 22, 0.2); color: #f97316; }
|
||||
|
||||
.item-tags {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: var(--text-xs);
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-surface-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.item-used {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--success);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1a1a2e;
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-5);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
[data-theme="light"] .modal-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: var(--space-1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin-top: var(--space-4);
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<a href="index.html" class="logo">
|
||||
<i data-lucide="circle-dot"></i>
|
||||
Echo
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="index.html" class="nav-item">
|
||||
<i data-lucide="layout-list"></i>
|
||||
<span>Tasks</span>
|
||||
</a>
|
||||
<a href="notes.html" class="nav-item">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>Notes</span>
|
||||
</a>
|
||||
<a href="files.html" class="nav-item">
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="grup-sprijin.html" class="nav-item active">
|
||||
<i data-lucide="heart-handshake"></i>
|
||||
<span>Grup</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Grup Sprijin - Exerciții & Întrebări</h1>
|
||||
<div class="search-bar">
|
||||
<input type="text" class="input" placeholder="Caută..." id="searchInput">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="stats"></div>
|
||||
|
||||
<div class="fise-section" id="fiseSection" style="margin-bottom: var(--space-5); display: none;">
|
||||
<h2 style="font-size: var(--text-lg); margin-bottom: var(--space-3); color: var(--text-primary);">Fișe întâlniri</h2>
|
||||
<div class="fise-list" id="fiseList" style="display: flex; gap: var(--space-2); flex-wrap: wrap;"></div>
|
||||
</div>
|
||||
|
||||
<div class="filters" id="filters">
|
||||
<button class="filter-btn active" data-filter="all">Toate</button>
|
||||
<button class="filter-btn" data-filter="exercitiu">Exerciții</button>
|
||||
<button class="filter-btn" data-filter="meditatie">Meditații</button>
|
||||
<button class="filter-btn" data-filter="intrebare">Întrebări</button>
|
||||
<button class="filter-btn" data-filter="reflectie">Reflecții</button>
|
||||
<button class="filter-btn" data-filter="unused">Nefolosite</button>
|
||||
<button class="filter-btn" data-filter="used">Folosite</button>
|
||||
</div>
|
||||
|
||||
<div class="items-grid" id="itemsGrid">
|
||||
<p>Se încarcă...</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title" id="modalTitle"></h2>
|
||||
<span class="item-type" id="modalType"></span>
|
||||
</div>
|
||||
<button class="modal-close" onclick="closeModal()">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody"></div>
|
||||
<div class="item-tags" id="modalTags"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" id="markUsedBtn" onclick="toggleUsed()">Marchează folosit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Theme
|
||||
function toggleTheme() {
|
||||
const body = document.body;
|
||||
const current = body.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
body.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateThemeIcon();
|
||||
}
|
||||
|
||||
function updateThemeIcon() {
|
||||
const theme = document.body.getAttribute('data-theme') || 'dark';
|
||||
const icon = document.getElementById('themeIcon');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.body.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Data
|
||||
const API_BASE = window.location.pathname.includes('/echo/') ? '/echo' : '';
|
||||
let items = [];
|
||||
let currentFilter = 'all';
|
||||
let currentItem = null;
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
const response = await fetch('grup-sprijin/index.json?t=' + Date.now());
|
||||
if (!response.ok) throw new Error('Nu am găsit fișierul');
|
||||
items = await response.json();
|
||||
render();
|
||||
} catch (e) {
|
||||
console.error('Error loading items:', e);
|
||||
document.getElementById('itemsGrid').innerHTML = `
|
||||
<div class="error-msg">
|
||||
Eroare la încărcare: ${e.message}<br>
|
||||
<small>Verifică dacă fișierul grup-sprijin/index.json există</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
let filtered = items.filter(item => {
|
||||
if (search && !item.title.toLowerCase().includes(search) &&
|
||||
!item.content.toLowerCase().includes(search) &&
|
||||
!item.tags.some(t => t.toLowerCase().includes(search))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentFilter === 'all') return true;
|
||||
if (currentFilter === 'used') return item.used;
|
||||
if (currentFilter === 'unused') return !item.used;
|
||||
return item.type === currentFilter;
|
||||
});
|
||||
|
||||
const total = items.length;
|
||||
const used = items.filter(i => i.used).length;
|
||||
document.getElementById('stats').innerHTML = `
|
||||
<span>Total: ${total}</span>
|
||||
<span>Folosite: ${used}</span>
|
||||
<span>Nefolosite: ${total - used}</span>
|
||||
`;
|
||||
|
||||
const grid = document.getElementById('itemsGrid');
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = '<p style="color: var(--text-muted);">Niciun rezultat</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = filtered.map(item => `
|
||||
<div class="item-card ${item.used ? 'used' : ''}" onclick="openModal('${item.id}')">
|
||||
<div class="item-header">
|
||||
<span class="item-title">${item.title}</span>
|
||||
<span class="item-type ${item.type}">${item.type}</span>
|
||||
</div>
|
||||
<div class="item-tags">
|
||||
${item.tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
||||
</div>
|
||||
${item.used ? `<div class="item-used">✓ Folosit: ${item.used}</div>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
currentItem = items.find(i => i.id === id);
|
||||
if (!currentItem) return;
|
||||
|
||||
document.getElementById('modalTitle').textContent = currentItem.title;
|
||||
document.getElementById('modalType').textContent = currentItem.type;
|
||||
document.getElementById('modalType').className = `item-type ${currentItem.type}`;
|
||||
document.getElementById('modalBody').textContent = currentItem.content;
|
||||
document.getElementById('modalTags').innerHTML = currentItem.tags.map(t => `<span class="tag">${t}</span>`).join('');
|
||||
document.getElementById('markUsedBtn').textContent = currentItem.used ? 'Marchează nefolosit' : 'Marchează folosit';
|
||||
|
||||
document.getElementById('modal').classList.add('open');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('open');
|
||||
currentItem = null;
|
||||
}
|
||||
|
||||
async function toggleUsed() {
|
||||
if (!currentItem) return;
|
||||
|
||||
const idx = items.findIndex(i => i.id === currentItem.id);
|
||||
if (idx === -1) return;
|
||||
|
||||
if (items[idx].used) {
|
||||
items[idx].used = null;
|
||||
} else {
|
||||
items[idx].used = new Date().toLocaleDateString('ro-RO');
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: 'grup-sprijin/index.json',
|
||||
content: JSON.stringify(items, null, 2)
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error saving:', e);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
render();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentFilter = btn.dataset.filter;
|
||||
render();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('input', render);
|
||||
|
||||
document.getElementById('modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal') closeModal();
|
||||
});
|
||||
|
||||
// Load fise
|
||||
async function loadFise() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/files?path=kanban/grup-sprijin&action=list`);
|
||||
const data = await response.json();
|
||||
if (data.items) {
|
||||
const fise = data.items.filter(f => f.name.startsWith('fisa-') && f.name.endsWith('.md'));
|
||||
if (fise.length > 0) {
|
||||
document.getElementById('fiseSection').style.display = 'block';
|
||||
document.getElementById('fiseList').innerHTML = fise.map(f => `
|
||||
<a href="files.html#kanban/grup-sprijin/${f.name}" class="filter-btn" style="text-decoration: none;">
|
||||
${f.name.replace('fisa-', '').replace('.md', '')}
|
||||
</a>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No fise yet');
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
loadItems();
|
||||
loadFise();
|
||||
lucide.createIcons();
|
||||
updateThemeIcon();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1839
dashboard/index.html
Normal file
1839
dashboard/index.html
Normal file
File diff suppressed because it is too large
Load Diff
29
dashboard/issues.json
Normal file
29
dashboard/issues.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"lastUpdated": "2026-01-30T17:37:00.676Z",
|
||||
"programs": [
|
||||
"ROACONT",
|
||||
"ROAGEST",
|
||||
"ROAIMOB",
|
||||
"ROAFACTURARE",
|
||||
"ROADEF",
|
||||
"ROASTART",
|
||||
"ROAPRINT",
|
||||
"ROAWEB",
|
||||
"Clawdbot",
|
||||
"Personal",
|
||||
"Altele"
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"id": "ROA-001",
|
||||
"title": "D101: Mutare impozit precedent RD49→RD50",
|
||||
"description": "RD 49 = în urma inspecției fiscale\nRD 50 = impozit precedent\nFormularul nu recalculează impozitul de 16%\nRD 40 se modifică și la 4.1",
|
||||
"program": "ROACONT",
|
||||
"owner": "marius",
|
||||
"priority": "urgent-important",
|
||||
"status": "todo",
|
||||
"created": "2026-01-30T15:10:00Z",
|
||||
"deadline": null
|
||||
}
|
||||
]
|
||||
}
|
||||
1
dashboard/memory
Symbolic link
1
dashboard/memory
Symbolic link
@@ -0,0 +1 @@
|
||||
../memory
|
||||
1
dashboard/notes-data
Symbolic link
1
dashboard/notes-data
Symbolic link
@@ -0,0 +1 @@
|
||||
../kb
|
||||
1214
dashboard/notes.html
Normal file
1214
dashboard/notes.html
Normal file
File diff suppressed because it is too large
Load Diff
4
dashboard/status.json
Normal file
4
dashboard/status.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"git": {"status": "4 fișiere", "clean": false, "files": 4},
|
||||
"lastReport": {"type": "evening", "summary": "notes.html îmbunătățit (filtre colorate), rețetă salvată", "time": "30 Jan 2026, 22:00"}
|
||||
}
|
||||
123
dashboard/swipe-nav.js
Normal file
123
dashboard/swipe-nav.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Swipe Navigation for Echo
|
||||
* Swipe left/right to navigate between pages
|
||||
*/
|
||||
(function() {
|
||||
const pages = ['index.html', 'notes.html', 'files.html'];
|
||||
|
||||
// Get current page index
|
||||
function getCurrentIndex() {
|
||||
const path = window.location.pathname;
|
||||
let filename = path.split('/').pop() || 'index.html';
|
||||
// Handle /echo/ without filename
|
||||
if (filename === '' || filename === 'echo') filename = 'index.html';
|
||||
const idx = pages.indexOf(filename);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
// Navigate to page
|
||||
function navigateTo(index) {
|
||||
if (index >= 0 && index < pages.length) {
|
||||
window.location.href = pages[index];
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe detection
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchEndX = 0;
|
||||
let touchEndY = 0;
|
||||
|
||||
const minSwipeDistance = 80;
|
||||
const maxVerticalDistance = 100;
|
||||
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
touchStartY = e.changedTouches[0].screenY;
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', function(e) {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
touchEndY = e.changedTouches[0].screenY;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
function handleSwipe() {
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
const deltaY = Math.abs(touchEndY - touchStartY);
|
||||
|
||||
// Ignore if vertical swipe or too short
|
||||
if (deltaY > maxVerticalDistance) return;
|
||||
if (Math.abs(deltaX) < minSwipeDistance) return;
|
||||
|
||||
const currentIndex = getCurrentIndex();
|
||||
|
||||
if (deltaX > 0) {
|
||||
// Swipe right → previous page
|
||||
navigateTo(currentIndex - 1);
|
||||
} else {
|
||||
// Swipe left → next page
|
||||
navigateTo(currentIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Visual indicator (optional dots)
|
||||
function createIndicator() {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'swipe-indicator';
|
||||
indicator.innerHTML = pages.map((_, i) =>
|
||||
`<span class="swipe-dot ${i === getCurrentIndex() ? 'active' : ''}"></span>`
|
||||
).join('');
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
// Add indicator styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.swipe-indicator {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
z-index: 9999;
|
||||
padding: 10px 16px;
|
||||
background: rgba(50, 50, 60, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.swipe-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.swipe-dot.active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 8px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.swipe-indicator { display: none; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Init after DOM ready
|
||||
function init() {
|
||||
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
|
||||
createIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
237
dashboard/tasks.json
Normal file
237
dashboard/tasks.json
Normal file
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"lastUpdated": "2026-01-30T20:26:37.897978Z",
|
||||
"columns": [
|
||||
{
|
||||
"id": "backlog",
|
||||
"name": "Backlog",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-006",
|
||||
"title": "Email digest dimineața",
|
||||
"description": "Sumar email-uri importante la 8 AM",
|
||||
"created": "2025-01-30",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-007",
|
||||
"title": "Calendar sync",
|
||||
"description": "Alertă înainte de întâlniri Google Calendar",
|
||||
"created": "2025-01-30",
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "in-progress",
|
||||
"name": "In Progress",
|
||||
"tasks": []
|
||||
},
|
||||
{
|
||||
"id": "done",
|
||||
"name": "Done",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-029",
|
||||
"title": "Test sortare timestamp",
|
||||
"description": "Verificare sortare",
|
||||
"created": "2026-01-29T14:54:17Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29T14:54:25Z"
|
||||
},
|
||||
{
|
||||
"id": "task-027",
|
||||
"title": "UI fixes: kanban icons + notes tags",
|
||||
"description": "Scos emoji din coloane kanban. Adăugat tag pills cu multi-select și count în notes.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-026",
|
||||
"title": "Swipe navigation mobil",
|
||||
"description": "Swipe stânga/dreapta pentru navigare între Tasks ↔ Notes ↔ Files. Indicator dots pe mobil.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-025",
|
||||
"title": "Notes: Accordion pe zile",
|
||||
"description": "Grupare: Azi (expanded), Ieri, Săptămâna aceasta, Mai vechi (collapsed). Click pentru expand/collapse.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-024",
|
||||
"title": "Fix contrast dark/light mode",
|
||||
"description": "Text și borders mai vizibile, header alb în light mode, toggle temă funcțional",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-023",
|
||||
"title": "Design System Unificat",
|
||||
"description": "common.css + Lucide icons + UI modern pe toate paginile: Tasks, Notes, Files",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-022",
|
||||
"title": "Unificare stil navigare",
|
||||
"description": "Nav unificat pe toate paginile: 📋 Tasks | 📝 Notes | 📁 Files cu iconuri și stil consistent",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-021",
|
||||
"title": "UI/UX Redesign v2",
|
||||
"description": "Kanban: doar In Progress expandat. Notes: mobile tabs. Files: Browse/Editor tabs cu grid.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-020",
|
||||
"title": "UI Responsive & Compact",
|
||||
"description": "Coloane colapsabile, task-uri compacte (click expand), sidebar toggle, Done minimizat by default",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-019",
|
||||
"title": "Comparare bilanț 12/2025 vs 12/2024",
|
||||
"description": "Doar S1002 modificat! Câmpuri noi: AN_CAEN, d_audit_intern. Raport: bilant_compare/2025_vs_2024/",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-018",
|
||||
"title": "Comparare bilanț ANAF 2024 vs 2023",
|
||||
"description": "Comparat XSD-uri S1002-S1005. Raport: anaf-monitor/bilant_compare/RAPORT_DIFERENTE_2024_vs_2023.md",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-017",
|
||||
"title": "Scrie un haiku",
|
||||
"description": "Biți în noaptea grea / Claude răspunde în liniște / Ecou digital",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-005",
|
||||
"title": "Kanban board",
|
||||
"description": "Interfață web pentru vizualizare task-uri",
|
||||
"created": "2025-01-30",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-008",
|
||||
"title": "YouTube Notes interface",
|
||||
"description": "Interfață pentru vizualizare notițe cu search",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-009",
|
||||
"title": "Search în notițe",
|
||||
"description": "Căutare în titlu, tags și conținut",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-010",
|
||||
"title": "Sumarizare: Claude Code Do Work Pattern",
|
||||
"description": "https://youtu.be/I9-tdhxiH7w",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-011",
|
||||
"title": "File Explorer în Task Board",
|
||||
"description": "Interfață pentru browse/edit fișiere din workspace",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-013",
|
||||
"title": "Kanban interactiv cu drag & drop",
|
||||
"description": "Adăugat: drag-drop, add/edit/delete tasks, priorități, salvare automată",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-014",
|
||||
"title": "Sumarizare: It Got Worse (Clawdbot)...",
|
||||
"description": "https://youtu.be/rPAKq2oQVBs?si=6sJk41XsCrQQt6Lg",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-015",
|
||||
"title": "Sumarizare: Greșeli post cu apă",
|
||||
"description": "https://youtu.be/4QjkI0sf64M",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-016",
|
||||
"title": "Sumarizare: GSD Framework Claude Code",
|
||||
"description": "https://www.youtube.com/watch?v=l94A53kIUB0",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-028",
|
||||
"title": "ANAF Monitor - verificare (test)",
|
||||
"description": "Testare manuală cron job",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-030",
|
||||
"title": "Test task tracking",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:12:25Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:12:29Z"
|
||||
},
|
||||
{
|
||||
"id": "task-031",
|
||||
"title": "Fix notes tag coloring on expand",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:16:46Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:17:08Z"
|
||||
},
|
||||
{
|
||||
"id": "task-032",
|
||||
"title": "Fix cron jobs timezone Bucharest",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:21:26Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:21:44Z"
|
||||
},
|
||||
{
|
||||
"id": "task-033",
|
||||
"title": "Redirect coaching to @health, reports to @work",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:25:22Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:26:37Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
120
dashboard/update_task.py
Normal file
120
dashboard/update_task.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper script for Echo to update kanban tasks.
|
||||
Usage: python3 update_task.py <action> <args>
|
||||
|
||||
Actions:
|
||||
add <column> <title> [description] [priority]
|
||||
move <task_id> <to_column>
|
||||
done <task_id>
|
||||
list
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
TASKS_FILE = Path(__file__).parent / 'tasks.json'
|
||||
|
||||
def load_tasks():
|
||||
with open(TASKS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_tasks(data):
|
||||
data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
|
||||
with open(TASKS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def get_next_id(data):
|
||||
max_id = 0
|
||||
for col in data['columns']:
|
||||
for task in col['tasks']:
|
||||
num = int(task['id'].split('-')[1])
|
||||
if num > max_id:
|
||||
max_id = num
|
||||
return f"task-{max_id + 1:03d}"
|
||||
|
||||
def add_task(column_id, title, description="", priority="medium"):
|
||||
data = load_tasks()
|
||||
new_task = {
|
||||
"id": get_next_id(data),
|
||||
"title": title,
|
||||
"description": description,
|
||||
"created": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"priority": priority
|
||||
}
|
||||
|
||||
for col in data['columns']:
|
||||
if col['id'] == column_id:
|
||||
col['tasks'].append(new_task)
|
||||
save_tasks(data)
|
||||
print(f"Added: {new_task['id']} - {title}")
|
||||
return
|
||||
|
||||
print(f"Column not found: {column_id}")
|
||||
|
||||
def move_task(task_id, to_column):
|
||||
data = load_tasks()
|
||||
task = None
|
||||
|
||||
# Find and remove task
|
||||
for col in data['columns']:
|
||||
for t in col['tasks']:
|
||||
if t['id'] == task_id:
|
||||
task = t
|
||||
col['tasks'].remove(t)
|
||||
break
|
||||
if task:
|
||||
break
|
||||
|
||||
if not task:
|
||||
print(f"Task not found: {task_id}")
|
||||
return
|
||||
|
||||
# Add to new column
|
||||
if to_column == 'done':
|
||||
task['completed'] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
for col in data['columns']:
|
||||
if col['id'] == to_column:
|
||||
col['tasks'].append(task)
|
||||
save_tasks(data)
|
||||
print(f"Moved: {task_id} -> {to_column}")
|
||||
return
|
||||
|
||||
print(f"Column not found: {to_column}")
|
||||
|
||||
def done_task(task_id):
|
||||
move_task(task_id, 'done')
|
||||
|
||||
def list_tasks():
|
||||
data = load_tasks()
|
||||
for col in data['columns']:
|
||||
print(f"\n{col['name']} ({len(col['tasks'])})")
|
||||
print("-" * 40)
|
||||
for task in col['tasks']:
|
||||
print(f" [{task['id']}] {task['title']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
|
||||
if action == 'add' and len(sys.argv) >= 4:
|
||||
add_task(
|
||||
sys.argv[2], # column
|
||||
sys.argv[3], # title
|
||||
sys.argv[4] if len(sys.argv) > 4 else "",
|
||||
sys.argv[5] if len(sys.argv) > 5 else "medium"
|
||||
)
|
||||
elif action == 'move' and len(sys.argv) >= 4:
|
||||
move_task(sys.argv[2], sys.argv[3])
|
||||
elif action == 'done' and len(sys.argv) >= 3:
|
||||
done_task(sys.argv[2])
|
||||
elif action == 'list':
|
||||
list_tasks()
|
||||
else:
|
||||
print(__doc__)
|
||||
1
dashboard/youtube-notes
Symbolic link
1
dashboard/youtube-notes
Symbolic link
@@ -0,0 +1 @@
|
||||
../kb/youtube
|
||||
Reference in New Issue
Block a user