Update dashboard, memory, root (+7 ~14)
This commit is contained in:
281
dashboard/api.py
281
dashboard/api.py
@@ -28,6 +28,12 @@ KANBAN_DIR = BASE_DIR / 'dashboard'
|
||||
WORKSPACE_DIR = Path('/home/moltbot/workspace')
|
||||
HABITS_FILE = KANBAN_DIR / 'habits.json'
|
||||
|
||||
# Eco (echo-core) constants
|
||||
ECO_SERVICES = ['echo-core', 'echo-whatsapp-bridge', 'echo-taskboard']
|
||||
ECHO_CORE_DIR = Path('/home/moltbot/echo-core')
|
||||
ECHO_LOG_FILE = ECHO_CORE_DIR / 'logs' / 'echo-core.log'
|
||||
ECHO_SESSIONS_FILE = ECHO_CORE_DIR / 'sessions' / 'active.json'
|
||||
|
||||
# Load .env file if present
|
||||
_env_file = Path(__file__).parent / '.env'
|
||||
if _env_file.exists():
|
||||
@@ -70,6 +76,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
self.handle_workspace_git_push()
|
||||
elif self.path == '/api/workspace/delete':
|
||||
self.handle_workspace_delete()
|
||||
elif self.path == '/api/eco/restart':
|
||||
self.handle_eco_restart()
|
||||
elif self.path == '/api/eco/stop':
|
||||
self.handle_eco_stop()
|
||||
elif self.path == '/api/eco/sessions/clear':
|
||||
self.handle_eco_sessions_clear()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
@@ -289,6 +301,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
self.handle_workspace_git_diff()
|
||||
elif self.path.startswith('/api/workspace/logs'):
|
||||
self.handle_workspace_logs()
|
||||
elif self.path == '/api/eco/status' or self.path.startswith('/api/eco/status?'):
|
||||
self.handle_eco_status()
|
||||
elif self.path.startswith('/api/eco/logs'):
|
||||
self.handle_eco_logs()
|
||||
elif self.path == '/api/eco/doctor':
|
||||
self.handle_eco_doctor()
|
||||
elif self.path.startswith('/api/'):
|
||||
self.send_error(404)
|
||||
else:
|
||||
@@ -1949,6 +1967,269 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
# ── Eco (echo-core) handlers ──────────────────────────────────────
|
||||
|
||||
def handle_eco_status(self):
|
||||
"""Get status of echo-core services + active sessions."""
|
||||
try:
|
||||
services = []
|
||||
for svc in ECO_SERVICES:
|
||||
info = {'name': svc, 'active': False, 'pid': None, 'uptime': None, 'memory': None}
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'is-active', svc],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
info['active'] = result.stdout.strip() == 'active'
|
||||
|
||||
if info['active']:
|
||||
# PID
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'show', '-p', 'MainPID', '--value', svc],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
pid = result.stdout.strip()
|
||||
if pid and pid != '0':
|
||||
info['pid'] = int(pid)
|
||||
|
||||
# Uptime via systemctl timestamp
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['systemctl', '--user', 'show', '-p', 'ActiveEnterTimestamp', '--value', svc],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
ts = r.stdout.strip()
|
||||
if ts:
|
||||
start = datetime.strptime(ts, '%a %Y-%m-%d %H:%M:%S %Z')
|
||||
info['uptime'] = int((datetime.utcnow() - start).total_seconds())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Memory (VmRSS from /proc)
|
||||
try:
|
||||
for line in Path(f'/proc/{pid}/status').read_text().splitlines():
|
||||
if line.startswith('VmRSS:'):
|
||||
info['memory'] = line.split(':')[1].strip()
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
services.append(info)
|
||||
|
||||
# Active sessions
|
||||
sessions = []
|
||||
if ECHO_SESSIONS_FILE.exists():
|
||||
try:
|
||||
sessions = json.loads(ECHO_SESSIONS_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.send_json({'services': services, 'sessions': sessions})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_restart(self):
|
||||
"""Restart an echo-core service (not taskboard)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
svc = data.get('service', '')
|
||||
|
||||
if svc not in ECO_SERVICES:
|
||||
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
|
||||
return
|
||||
if svc == 'echo-taskboard':
|
||||
self.send_json({'success': False, 'error': 'Cannot restart taskboard from itself'}, 400)
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'restart', svc],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.send_json({'success': True, 'message': f'{svc} restarted'})
|
||||
else:
|
||||
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_stop(self):
|
||||
"""Stop an echo-core service (not taskboard)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
svc = data.get('service', '')
|
||||
|
||||
if svc not in ECO_SERVICES:
|
||||
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
|
||||
return
|
||||
if svc == 'echo-taskboard':
|
||||
self.send_json({'success': False, 'error': 'Cannot stop taskboard from itself'}, 400)
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'stop', svc],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.send_json({'success': True, 'message': f'{svc} stopped'})
|
||||
else:
|
||||
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_logs(self):
|
||||
"""Return last N lines from echo-core.log."""
|
||||
try:
|
||||
params = parse_qs(urlparse(self.path).query)
|
||||
lines = min(int(params.get('lines', ['100'])[0]), 500)
|
||||
|
||||
if not ECHO_LOG_FILE.exists():
|
||||
self.send_json({'lines': ['(log file not found)']})
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['tail', '-n', str(lines), str(ECHO_LOG_FILE)],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
self.send_json({'lines': result.stdout.splitlines()})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_doctor(self):
|
||||
"""Run health checks on echo-core ecosystem."""
|
||||
checks = []
|
||||
|
||||
# 1. Services
|
||||
for svc in ECO_SERVICES:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['systemctl', '--user', 'is-active', svc],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
active = r.stdout.strip() == 'active'
|
||||
checks.append({
|
||||
'name': f'Service: {svc}',
|
||||
'pass': active,
|
||||
'detail': 'active' if active else r.stdout.strip()
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': f'Service: {svc}', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 2. Disk space
|
||||
try:
|
||||
st = shutil.disk_usage('/')
|
||||
pct_free = (st.free / st.total) * 100
|
||||
checks.append({
|
||||
'name': 'Disk space',
|
||||
'pass': pct_free > 5,
|
||||
'detail': f'{pct_free:.1f}% free ({st.free // (1024**3)} GB)'
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Disk space', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 3. Log file
|
||||
try:
|
||||
if ECHO_LOG_FILE.exists():
|
||||
size = ECHO_LOG_FILE.stat().st_size
|
||||
size_mb = size / (1024 * 1024)
|
||||
checks.append({
|
||||
'name': 'Log file',
|
||||
'pass': size_mb < 100,
|
||||
'detail': f'{size_mb:.1f} MB'
|
||||
})
|
||||
else:
|
||||
checks.append({'name': 'Log file', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Log file', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 4. Sessions file
|
||||
try:
|
||||
if ECHO_SESSIONS_FILE.exists():
|
||||
data = json.loads(ECHO_SESSIONS_FILE.read_text())
|
||||
count = len(data) if isinstance(data, list) else len(data.keys()) if isinstance(data, dict) else 0
|
||||
checks.append({'name': 'Sessions file', 'pass': True, 'detail': f'{count} active'})
|
||||
else:
|
||||
checks.append({'name': 'Sessions file', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Sessions file', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 5. Config
|
||||
config_file = ECHO_CORE_DIR / 'config.json'
|
||||
try:
|
||||
if config_file.exists():
|
||||
json.loads(config_file.read_text())
|
||||
checks.append({'name': 'Config', 'pass': True, 'detail': 'Valid JSON'})
|
||||
else:
|
||||
checks.append({'name': 'Config', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Config', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 6. WhatsApp bridge log
|
||||
wa_log = ECHO_CORE_DIR / 'logs' / 'whatsapp-bridge.log'
|
||||
try:
|
||||
if wa_log.exists():
|
||||
# Check last line for errors
|
||||
r = subprocess.run(
|
||||
['tail', '-1', str(wa_log)],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
last = r.stdout.strip()
|
||||
has_error = 'error' in last.lower() or 'fatal' in last.lower()
|
||||
checks.append({
|
||||
'name': 'WhatsApp bridge log',
|
||||
'pass': not has_error,
|
||||
'detail': last[:80] if last else 'Empty'
|
||||
})
|
||||
else:
|
||||
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 7. Claude CLI
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['which', 'claude'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
found = r.returncode == 0
|
||||
checks.append({
|
||||
'name': 'Claude CLI',
|
||||
'pass': found,
|
||||
'detail': r.stdout.strip() if found else 'Not in PATH'
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Claude CLI', 'pass': False, 'detail': str(e)})
|
||||
|
||||
self.send_json({'checks': checks})
|
||||
|
||||
def handle_eco_sessions_clear(self):
|
||||
"""Clear active sessions (all or specific channel)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
channel = data.get('channel', None)
|
||||
|
||||
if not ECHO_SESSIONS_FILE.exists():
|
||||
self.send_json({'success': True, 'message': 'No sessions file'})
|
||||
return
|
||||
|
||||
if channel:
|
||||
# Remove specific channel
|
||||
sessions = json.loads(ECHO_SESSIONS_FILE.read_text())
|
||||
if isinstance(sessions, list):
|
||||
sessions = [s for s in sessions if s.get('channel') != channel]
|
||||
elif isinstance(sessions, dict):
|
||||
sessions.pop(channel, None)
|
||||
ECHO_SESSIONS_FILE.write_text(json.dumps(sessions, indent=2))
|
||||
self.send_json({'success': True, 'message': f'Cleared session: {channel}'})
|
||||
else:
|
||||
# Clear all
|
||||
if isinstance(json.loads(ECHO_SESSIONS_FILE.read_text()), list):
|
||||
ECHO_SESSIONS_FILE.write_text('[]')
|
||||
else:
|
||||
ECHO_SESSIONS_FILE.write_text('{}')
|
||||
self.send_json({'success': True, 'message': 'All sessions cleared'})
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
|
||||
821
dashboard/eco.html
Normal file
821
dashboard/eco.html
Normal file
@@ -0,0 +1,821 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
|
||||
<title>Echo · Eco</title>
|
||||
<link rel="stylesheet" href="/echo/common.css">
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script src="/echo/swipe-nav.js"></script>
|
||||
<style>
|
||||
.main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.section {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* Service cards */
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
transition: border-color var(--transition-base);
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.status-dot.inactive {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.service-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.service-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.service-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.service-actions .btn {
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
}
|
||||
|
||||
/* Sessions table */
|
||||
.sessions-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sessions-table th,
|
||||
.sessions-table td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sessions-table th {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.sessions-table td {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.sessions-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-5);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sessions-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Log viewer */
|
||||
.log-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.log-toolbar label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.log-toolbar select {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: var(--space-4);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
/* Auto-refresh toggle */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-surface-active);
|
||||
border-radius: 20px;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 50%;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(16px);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Doctor checks */
|
||||
.doctor-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.check-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.check-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.check-icon.pass { color: var(--success); }
|
||||
.check-icon.fail { color: var(--error); }
|
||||
|
||||
.check-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.check-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--text-muted);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Button variants */
|
||||
.btn-danger {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
border-color: var(--warning);
|
||||
color: var(--warning);
|
||||
}
|
||||
.btn-warning:hover {
|
||||
background: var(--warning);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.service-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.check-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.check-name {
|
||||
min-width: auto;
|
||||
}
|
||||
.sessions-table {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.sessions-table th:nth-child(3),
|
||||
.sessions-table td:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<a href="/echo/index.html" class="logo">
|
||||
<i data-lucide="circle-dot"></i>
|
||||
Echo
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="/echo/index.html" class="nav-item">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/echo/workspace.html" class="nav-item">
|
||||
<i data-lucide="code"></i>
|
||||
<span>Workspace</span>
|
||||
</a>
|
||||
<a href="/echo/notes.html" class="nav-item">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>KB</span>
|
||||
</a>
|
||||
<a href="/echo/habits.html" class="nav-item">
|
||||
<i data-lucide="dumbbell"></i>
|
||||
<span>Habits</span>
|
||||
</a>
|
||||
<a href="/echo/files.html" class="nav-item">
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item active">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimba tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Eco Control Panel</h1>
|
||||
<div class="page-subtitle" id="statusSummary">Loading...</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="refreshAll()">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i data-lucide="server" style="width:18px;height:18px;"></i>
|
||||
Services
|
||||
</h2>
|
||||
</div>
|
||||
<div class="services-grid" id="servicesGrid">
|
||||
<div style="color:var(--text-muted);">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i data-lucide="message-square" style="width:18px;height:18px;"></i>
|
||||
Sessions
|
||||
</h2>
|
||||
<button class="btn btn-secondary btn-danger" onclick="clearAllSessions()" id="clearAllBtn" style="display:none;">
|
||||
<i data-lucide="trash-2"></i>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<div class="sessions-card">
|
||||
<div id="sessionsContent">
|
||||
<div class="sessions-empty">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i data-lucide="scroll-text" style="width:18px;height:18px;"></i>
|
||||
Logs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="log-card">
|
||||
<div class="log-toolbar">
|
||||
<label>Lines:</label>
|
||||
<select id="logLines" onchange="loadLogs()">
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" onclick="loadLogs()" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:var(--space-2);">
|
||||
<label style="font-size:var(--text-xs);color:var(--text-muted);">Auto</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="autoRefreshLogs" onchange="toggleAutoRefreshLogs()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content" id="logContent">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Doctor -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i data-lucide="stethoscope" style="width:18px;height:18px;"></i>
|
||||
Doctor
|
||||
</h2>
|
||||
<button class="btn btn-primary" onclick="runDoctor()" id="doctorBtn">
|
||||
<i data-lucide="play"></i>
|
||||
Run Doctor
|
||||
</button>
|
||||
</div>
|
||||
<div class="doctor-card" id="doctorCard" style="display:none;">
|
||||
<ul class="check-list" id="doctorChecks"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let statusRefreshInterval = null;
|
||||
let logRefreshInterval = null;
|
||||
|
||||
// Theme
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
}
|
||||
(function() {
|
||||
const saved = localStorage.getItem('theme');
|
||||
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
||||
})();
|
||||
|
||||
function formatUptime(seconds) {
|
||||
if (!seconds && seconds !== 0) return '-';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return `${d}d ${h}h`;
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
function svcLabel(name) {
|
||||
return name.replace('echo-', '').replace('-', ' ');
|
||||
}
|
||||
|
||||
// ── Status ──────────────────────────────────────────────
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/status');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
document.getElementById('servicesGrid').innerHTML =
|
||||
`<div style="color:var(--error);">${data.error}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
renderServices(data.services);
|
||||
renderSessions(data.sessions);
|
||||
|
||||
const active = data.services.filter(s => s.active).length;
|
||||
document.getElementById('statusSummary').textContent =
|
||||
`${active}/${data.services.length} services active`;
|
||||
} catch (e) {
|
||||
document.getElementById('servicesGrid').innerHTML =
|
||||
`<div style="color:var(--error);">Error: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderServices(services) {
|
||||
const grid = document.getElementById('servicesGrid');
|
||||
grid.innerHTML = services.map(svc => {
|
||||
const isTaskboard = svc.name === 'echo-taskboard';
|
||||
const canControl = !isTaskboard;
|
||||
|
||||
let actionsHtml = '';
|
||||
if (canControl) {
|
||||
if (svc.active) {
|
||||
actionsHtml = `
|
||||
<button class="btn btn-secondary" onclick="restartService('${svc.name}')">
|
||||
<i data-lucide="rotate-cw"></i> Restart
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-danger" onclick="stopService('${svc.name}')">
|
||||
<i data-lucide="square"></i> Stop
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
actionsHtml = `
|
||||
<button class="btn btn-primary" onclick="restartService('${svc.name}')">
|
||||
<i data-lucide="play"></i> Start
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<div class="service-name">
|
||||
<span class="status-dot ${svc.active ? 'active' : 'inactive'}"></span>
|
||||
${svcLabel(svc.name)}
|
||||
</div>
|
||||
<span style="font-size:var(--text-xs);color:${svc.active ? 'var(--success)' : 'var(--error)'};">
|
||||
${svc.active ? 'running' : 'stopped'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="service-meta">
|
||||
<span><i data-lucide="hash" style="width:12px;height:12px;"></i> PID: ${svc.pid || '-'}</span>
|
||||
<span><i data-lucide="clock" style="width:12px;height:12px;"></i> Uptime: ${formatUptime(svc.uptime)}</span>
|
||||
<span><i data-lucide="memory-stick" style="width:12px;height:12px;"></i> Memory: ${svc.memory || '-'}</span>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
${actionsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
async function restartService(name) {
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/restart', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({service: name})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setTimeout(loadStatus, 1500);
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopService(name) {
|
||||
if (!confirm(`Stop ${svcLabel(name)}?`)) return;
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/stop', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({service: name})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setTimeout(loadStatus, 1000);
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sessions ────────────────────────────────────────────
|
||||
|
||||
function renderSessions(sessions) {
|
||||
const container = document.getElementById('sessionsContent');
|
||||
const clearBtn = document.getElementById('clearAllBtn');
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
container.innerHTML = '<div class="sessions-empty">No active sessions</div>';
|
||||
clearBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
clearBtn.style.display = '';
|
||||
|
||||
// Handle both array and object formats
|
||||
let rows = [];
|
||||
if (Array.isArray(sessions)) {
|
||||
rows = sessions;
|
||||
} else if (typeof sessions === 'object') {
|
||||
rows = Object.entries(sessions).map(([k, v]) => ({
|
||||
channel: k,
|
||||
...(typeof v === 'object' ? v : {value: v})
|
||||
}));
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
container.innerHTML = '<div class="sessions-empty">No active sessions</div>';
|
||||
clearBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<table class="sessions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Platform</th>
|
||||
<th>Started</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(s => `
|
||||
<tr>
|
||||
<td>${escapeHtml(s.channel || s.id || '-')}</td>
|
||||
<td>${escapeHtml(s.platform || s.type || '-')}</td>
|
||||
<td>${s.started || s.timestamp || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-danger" style="font-size:var(--text-xs);padding:2px 8px;"
|
||||
onclick="clearSession('${escapeHtml(s.channel || s.id || '')}')">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
async function clearSession(channel) {
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/sessions/clear', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({channel: channel})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) loadStatus();
|
||||
else alert('Error: ' + data.error);
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllSessions() {
|
||||
if (!confirm('Clear all active sessions?')) return;
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/sessions/clear', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) loadStatus();
|
||||
else alert('Error: ' + data.error);
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Logs ────────────────────────────────────────────────
|
||||
|
||||
async function loadLogs() {
|
||||
const lines = document.getElementById('logLines').value;
|
||||
const content = document.getElementById('logContent');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/echo/api/eco/logs?lines=${lines}`);
|
||||
const data = await res.json();
|
||||
content.textContent = (data.lines || []).join('\n') || '(empty)';
|
||||
content.scrollTop = content.scrollHeight;
|
||||
} catch (e) {
|
||||
content.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRefreshLogs() {
|
||||
const enabled = document.getElementById('autoRefreshLogs').checked;
|
||||
if (logRefreshInterval) {
|
||||
clearInterval(logRefreshInterval);
|
||||
logRefreshInterval = null;
|
||||
}
|
||||
if (enabled) {
|
||||
logRefreshInterval = setInterval(loadLogs, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Doctor ──────────────────────────────────────────────
|
||||
|
||||
async function runDoctor() {
|
||||
const card = document.getElementById('doctorCard');
|
||||
const list = document.getElementById('doctorChecks');
|
||||
const btn = document.getElementById('doctorBtn');
|
||||
|
||||
card.style.display = '';
|
||||
list.innerHTML = '<li class="check-item"><span class="spinner"></span> Running checks...</li>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/echo/api/eco/doctor');
|
||||
const data = await res.json();
|
||||
|
||||
list.innerHTML = data.checks.map(c => `
|
||||
<li class="check-item">
|
||||
<span class="check-icon ${c.pass ? 'pass' : 'fail'}">
|
||||
${c.pass ? '✓' : '✗'}
|
||||
</span>
|
||||
<span class="check-name">${escapeHtml(c.name)}</span>
|
||||
<span class="check-detail">${escapeHtml(c.detail)}</span>
|
||||
</li>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
list.innerHTML = `<li class="check-item" style="color:var(--error);">Error: ${escapeHtml(e.message)}</li>`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
loadStatus();
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
// ── Init ────────────────────────────────────────────────
|
||||
|
||||
loadStatus();
|
||||
loadLogs();
|
||||
lucide.createIcons();
|
||||
|
||||
// Auto-refresh status every 10s
|
||||
statusRefreshInterval = setInterval(loadStatus, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -854,6 +854,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
|
||||
@@ -1458,6 +1458,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"lastUpdated": "2026-02-12T10:54:25.858148",
|
||||
"lastUpdated": "2026-02-14T06:36:41.910996",
|
||||
"habits": [
|
||||
{
|
||||
"id": "95c15eef-3a14-4985-a61e-0b64b72851b0",
|
||||
@@ -15,19 +15,27 @@
|
||||
"count": 5
|
||||
},
|
||||
"streak": {
|
||||
"current": 0,
|
||||
"current": 2,
|
||||
"best": 2,
|
||||
"lastCheckIn": "2026-02-12"
|
||||
"lastCheckIn": "2026-02-14"
|
||||
},
|
||||
"lives": 0,
|
||||
"completions": [
|
||||
{
|
||||
"date": "2026-02-11",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-13",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-14",
|
||||
"type": "check"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-02-11T00:54:03.447063",
|
||||
"updatedAt": "2026-02-12T10:54:25.858148"
|
||||
"updatedAt": "2026-02-14T06:36:41.910996"
|
||||
},
|
||||
{
|
||||
"id": "ceddaa7e-caf9-4038-94bb-da486c586bf8",
|
||||
@@ -43,19 +51,27 @@
|
||||
"count": 3
|
||||
},
|
||||
"streak": {
|
||||
"current": 1,
|
||||
"best": 1,
|
||||
"lastCheckIn": "2026-02-11"
|
||||
"current": 2,
|
||||
"best": 2,
|
||||
"lastCheckIn": "2026-02-14"
|
||||
},
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{
|
||||
"date": "2026-02-11",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-13",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-14",
|
||||
"type": "check"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-02-11T01:58:44.779904",
|
||||
"updatedAt": "2026-02-11T22:07:48.431522"
|
||||
"updatedAt": "2026-02-14T06:36:28.866187"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1073,6 +1073,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"lastUpdated": "2026-02-13T13:03:45.357Z",
|
||||
"lastUpdated": "2026-02-13T23:06:16.571Z",
|
||||
"programs": [
|
||||
"ROACONT",
|
||||
"ROAGEST",
|
||||
@@ -21,9 +21,10 @@
|
||||
"program": "ROACONT",
|
||||
"owner": "robert",
|
||||
"priority": "important",
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"created": "2026-02-12T13:19:01.786Z",
|
||||
"deadline": null
|
||||
"deadline": null,
|
||||
"completed": "2026-02-13T23:06:16.567Z"
|
||||
},
|
||||
{
|
||||
"id": "ROA-002",
|
||||
|
||||
@@ -704,6 +704,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Swipe left/right to navigate between pages
|
||||
*/
|
||||
(function() {
|
||||
const pages = ['index.html', 'notes.html', 'habits.html', 'files.html', 'workspace.html'];
|
||||
const pages = ['index.html', 'eco.html', 'notes.html', 'habits.html', 'files.html', 'workspace.html'];
|
||||
|
||||
// Get current page index
|
||||
function getCurrentIndex() {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
{
|
||||
"lastUpdated": "2026-02-13T13:03:30.654Z",
|
||||
"lastUpdated": "2026-02-14T07:00:00.000Z",
|
||||
"items": [
|
||||
{
|
||||
"id": "prov-2026-02-14",
|
||||
"text": "Provocare: Echilibrarea unui Conflict Interior - găsește un sau-sau și echilibrează-l",
|
||||
"context": "Găsește UN 'sau-sau' din viața ta — două lucruri pe care le consideri incompatibile. (1) Scrie conflictul: 'Sau sunt X, sau sunt Y'. (2) Pentru fiecare parte, găsește opusul simultan: Când ești X, cum ești deja și Y? (dovezi concrete). Când ești Y, cum ești deja și X? (dovezi concrete). (3) Observă: Când ambele sunt adevărate simultan, ce simți? Nu trebuie să rezolvi nimic — doar să vezi că cele două nu sunt incompatibile, sunt complementare. Metoda Demartini: echilibrezi percepția, nu elimini josurile.",
|
||||
"example": "Conflictul tău real: 'Sau sunt programator bun, sau sunt antreprenor.' Echilibrare: Când ești programator — deja faci antreprenoriat (ai firmă, negociezi cu clienți, iei decizii de business zilnic, ai angajat pe care îl formezi). Când ești antreprenor — deja folosești mintea tehnică (automatizezi, optimizezi, rezolvi probleme sistemic). Dovada: de 25 de ani faci AMBELE simultan. Doar percepția zice că una o exclude pe cealaltă.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-14",
|
||||
"done": false,
|
||||
"doneAt": null,
|
||||
"source": "Monica Ion - Povestea lui Marc Ep.9 (Anxietatea, frica de control și pierdere)",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/monica-ion-povestea-lui-marc-ep9-anxietatea.md",
|
||||
"createdAt": "2026-02-14T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-13",
|
||||
"text": "Provocare: Linkage Personal - conectează o activitate evitată cu calitățile tale",
|
||||
|
||||
@@ -448,6 +448,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/eco.html" class="nav-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Eco</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimba tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user