247 lines
9.4 KiB
Python
247 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Echo Task Board API — thin HTTP router.
|
|
|
|
All endpoint logic lives in `dashboard/handlers/*.py`. This file is
|
|
responsible only for URL dispatch, CORS, JSON response helpers, and
|
|
server bootstrap.
|
|
"""
|
|
import json
|
|
import sys
|
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
from pathlib import Path
|
|
|
|
# Make dashboard/ importable for the handler submodules (constants,
|
|
# habits_helpers, handlers.*). Tests rely on this as well.
|
|
_DASH = Path(__file__).parent
|
|
if str(_DASH) not in sys.path:
|
|
sys.path.insert(0, str(_DASH))
|
|
|
|
from constants import ( # noqa: E402 re-exported for tests
|
|
ALLOWED_WORKSPACES,
|
|
BASE_DIR,
|
|
ECHO_CORE_DIR,
|
|
ECHO_LOG_FILE,
|
|
ECHO_SESSIONS_FILE,
|
|
ECO_SERVICES,
|
|
GIT_WORKSPACE,
|
|
GITEA_ORG,
|
|
GITEA_TOKEN,
|
|
GITEA_URL,
|
|
HABITS_FILE,
|
|
KANBAN_DIR,
|
|
NOTES_DIR,
|
|
TOOLS_DIR,
|
|
VENV_PYTHON,
|
|
WORKSPACE_DIR,
|
|
)
|
|
from handlers.cron import CronHandlers # noqa: E402
|
|
from handlers.eco import EcoHandlers # noqa: E402
|
|
from handlers.files import FilesHandlers # noqa: E402
|
|
from handlers.git import GitHandlers # noqa: E402
|
|
from handlers.habits import HabitsHandlers # noqa: E402
|
|
from handlers.pdf import PDFHandlers # noqa: E402
|
|
from handlers.workspace import WorkspaceHandlers # noqa: E402
|
|
from handlers.youtube import YoutubeHandlers # noqa: E402
|
|
|
|
# Shared navigation injected into every served .html via <!--NAV--> marker.
|
|
NAV_HTML = '''<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" data-page="index">
|
|
<i data-lucide="layout-dashboard"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
<a href="/echo/workspace.html" class="nav-item" data-page="workspace">
|
|
<i data-lucide="code"></i>
|
|
<span>Workspace</span>
|
|
</a>
|
|
<a href="/echo/notes.html" class="nav-item" data-page="notes">
|
|
<i data-lucide="file-text"></i>
|
|
<span>KB</span>
|
|
</a>
|
|
<a href="/echo/habits.html" class="nav-item" data-page="habits">
|
|
<i data-lucide="dumbbell"></i>
|
|
<span>Habits</span>
|
|
</a>
|
|
<a href="/echo/files.html" class="nav-item" data-page="files">
|
|
<i data-lucide="folder"></i>
|
|
<span>Files</span>
|
|
</a>
|
|
<a href="/echo/grup-sprijin.html" class="nav-item" data-page="grup-sprijin">
|
|
<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>
|
|
<script>
|
|
(function(){
|
|
var path = window.location.pathname;
|
|
var m = path.match(/([^\\/]+?)(?:\\.html)?$/);
|
|
var page = m ? m[1] : 'index';
|
|
if (!page || page === 'echo') page = 'index';
|
|
var item = document.querySelector('.nav-item[data-page="' + page + '"]');
|
|
if (item) item.classList.add('active');
|
|
})();
|
|
</script>'''
|
|
|
|
|
|
class TaskBoardHandler(
|
|
GitHandlers,
|
|
HabitsHandlers,
|
|
EcoHandlers,
|
|
FilesHandlers,
|
|
PDFHandlers,
|
|
YoutubeHandlers,
|
|
WorkspaceHandlers,
|
|
CronHandlers,
|
|
SimpleHTTPRequestHandler,
|
|
):
|
|
"""HTTP request handler — dispatches to handler-mixin methods."""
|
|
|
|
# ── shared utilities ────────────────────────────────────────
|
|
def _read_post_json(self):
|
|
"""Read a JSON body from the POST request."""
|
|
content_length = int(self.headers['Content-Length'])
|
|
post_data = self.rfile.read(content_length).decode('utf-8')
|
|
return json.loads(post_data)
|
|
|
|
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.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
self.send_header('Pragma', 'no-cache')
|
|
self.send_header('Expires', '0')
|
|
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, PUT, DELETE, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
self.end_headers()
|
|
|
|
# ── dispatch ────────────────────────────────────────────────
|
|
def do_GET(self):
|
|
from datetime import datetime as _dt
|
|
if self.path == '/api/status':
|
|
self.send_json({'status': 'ok', 'time': _dt.now().isoformat()})
|
|
elif self.path == '/api/git' or self.path.startswith('/api/git?'):
|
|
self.handle_git_status()
|
|
elif self.path == '/api/cron' or self.path.startswith('/api/cron?'):
|
|
self.handle_cron_status()
|
|
elif self.path == '/api/habits':
|
|
self.handle_habits_get()
|
|
elif self.path.startswith('/api/files'):
|
|
self.handle_files_get()
|
|
elif self.path.startswith('/api/diff'):
|
|
self.handle_git_diff()
|
|
elif self.path == '/api/workspace' or self.path.startswith('/api/workspace?'):
|
|
self.handle_workspace_list()
|
|
elif self.path.startswith('/api/workspace/git/diff'):
|
|
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 == '/api/eco/sessions' or self.path.startswith('/api/eco/sessions?'):
|
|
self.handle_eco_sessions()
|
|
elif self.path.startswith('/api/eco/sessions/content'):
|
|
self.handle_eco_session_content()
|
|
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:
|
|
# Inject shared nav into served HTML pages via <!--NAV--> marker.
|
|
rel = self.path.lstrip('/').split('?')[0]
|
|
if rel.endswith('.html'):
|
|
try:
|
|
fpath = (KANBAN_DIR / rel).resolve()
|
|
fpath.relative_to(KANBAN_DIR.resolve())
|
|
except (ValueError, OSError):
|
|
self.send_error(403)
|
|
return
|
|
if fpath.is_file():
|
|
html = fpath.read_text('utf-8').replace('<!--NAV-->', NAV_HTML)
|
|
body = html.encode('utf-8')
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
self.send_header('Content-Length', str(len(body)))
|
|
self.send_header('Cache-Control', 'no-cache')
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
return
|
|
super().do_GET()
|
|
|
|
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()
|
|
elif self.path == '/api/pdf':
|
|
self.handle_pdf_post()
|
|
elif self.path == '/api/habits':
|
|
self.handle_habits_post()
|
|
elif self.path.startswith('/api/habits/') and self.path.endswith('/check'):
|
|
self.handle_habits_check()
|
|
elif self.path.startswith('/api/habits/') and self.path.endswith('/skip'):
|
|
self.handle_habits_skip()
|
|
elif self.path == '/api/workspace/run':
|
|
self.handle_workspace_run()
|
|
elif self.path == '/api/workspace/stop':
|
|
self.handle_workspace_stop()
|
|
elif self.path == '/api/workspace/git/commit':
|
|
self.handle_workspace_git_commit()
|
|
elif self.path == '/api/workspace/git/push':
|
|
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()
|
|
elif self.path == '/api/eco/git-commit':
|
|
self.handle_eco_git_commit()
|
|
elif self.path == '/api/eco/restart-taskboard':
|
|
self.handle_eco_restart_taskboard()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def do_PUT(self):
|
|
if self.path.startswith('/api/habits/'):
|
|
self.handle_habits_put()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def do_DELETE(self):
|
|
if self.path.startswith('/api/habits/') and '/check' in self.path:
|
|
self.handle_habits_uncheck()
|
|
elif self.path.startswith('/api/habits/'):
|
|
self.handle_habits_delete()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import os
|
|
port = 8088
|
|
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()
|