#!/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 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 == '/api/eco/git' or self.path.startswith('/api/eco/git?'): self.handle_eco_git_status() elif self.path.startswith('/api/'): self.send_error(404) else: 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()