#!/usr/bin/env python3 """ connectd/api.py - REST API for stats and control exposes daemon stats for home assistant integration. runs on port 8099 by default. """ import os import json import threading from http.server import HTTPServer, BaseHTTPRequestHandler from datetime import datetime from db import Database from db.users import get_priority_users, get_priority_user_matches, get_priority_user # central API config import requests CENTRAL_API = os.environ.get('CONNECTD_CENTRAL_API', '') CENTRAL_KEY = os.environ.get('CONNECTD_API_KEY', '') API_PORT = int(os.environ.get('CONNECTD_API_PORT', 8099)) # shared state (updated by daemon) _daemon_state = { 'running': False, 'dry_run': False, 'last_scout': None, 'last_match': None, 'last_intro': None, 'last_lost': None, 'intros_today': 0, 'lost_intros_today': 0, 'started_at': None, } def update_daemon_state(state_dict): """update shared daemon state (called by daemon)""" global _daemon_state _daemon_state.update(state_dict) def get_daemon_state(): """get current daemon state""" return _daemon_state.copy() DASHBOARD_HTML = """ connectd

connectd repo org

""" # draft cache - stores generated drafts so they dont regenerate _draft_cache = {} def get_cached_draft(match_id, match_type='match'): key = f"{match_type}:{match_id}" return _draft_cache.get(key) def cache_draft(match_id, draft_data, match_type='match'): key = f"{match_type}:{match_id}" _draft_cache[key] = draft_data class APIHandler(BaseHTTPRequestHandler): """simple REST API handler""" def log_message(self, format, *args): """suppress default logging""" pass def _send_json(self, data, status=200): """send JSON response""" self.send_response(status) 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_GET(self): """handle GET requests""" path = self.path.split('?')[0] if path == '/favicon.png' or path == '/favicon.ico': self._handle_favicon() elif path == '/' or path == '/dashboard': self._handle_dashboard() elif path == '/api/stats': self._handle_stats() elif path == '/api/host': self._handle_host() elif path == '/api/host_matches': self._handle_host_matches() elif path == '/api/your_matches': self._handle_your_matches() elif path == '/api/preview_match_draft': self._handle_preview_match_draft() elif path == '/api/preview_host_draft': self._handle_preview_host_draft() elif path == '/api/preview_draft': self._handle_preview_draft() elif path == '/api/pending_about_you': self._handle_pending_about_you() elif path == '/api/pending_to_you': self._handle_pending_to_you() elif path == '/api/pending_matches': self._handle_pending_matches() elif path == '/api/sent_intros': self._handle_sent_intros() elif path == '/api/failed_intros': self._handle_failed_intros() elif path == '/api/clear_cache': global _draft_cache _draft_cache = {} self._send_json({'status': 'cache cleared'}) elif path == '/api/health': self._handle_health() elif path == '/api/state': self._handle_state() elif path == '/api/priority_matches': self._handle_priority_matches() elif path == '/api/top_humans': self._handle_top_humans() elif path == '/api/user': self._handle_user() elif path == '/api/lost_builders': self._handle_lost_builders() else: self._send_json({'error': 'not found'}, 404) def _handle_favicon(self): from pathlib import Path fav = Path('/app/data/favicon.png') if fav.exists(): self.send_response(200) self.send_header('Content-Type', 'image/png') self.end_headers() self.wfile.write(fav.read_bytes()) else: self.send_response(404) self.end_headers() def _handle_dashboard(self): self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(DASHBOARD_HTML.encode()) def _handle_sent_intros(self): from pathlib import Path log_path = Path("/app/data/delivery_log.json") sent = [] if log_path.exists(): with open(log_path) as f: log = json.load(f) sent = log.get("sent", [])[-20:] sent.reverse() self._send_json({"sent": sent}) def _handle_failed_intros(self): from pathlib import Path log_path = Path("/app/data/delivery_log.json") failed = [] if log_path.exists(): with open(log_path) as f: log = json.load(f) failed = log.get("failed", []) self._send_json({"failed": failed}) def _handle_host(self): """daemon status and match stats""" import sqlite3 state = get_daemon_state() try: conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("SELECT COUNT(*) FROM matches WHERE status='pending' AND overlap_score >= 60") pending = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE status='intro_sent'") sent = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE status='rejected'") rejected = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches") total = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE overlap_score >= 90") s90 = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE overlap_score >= 80 AND overlap_score < 90") s80 = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE overlap_score >= 70 AND overlap_score < 80") s70 = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM matches WHERE overlap_score >= 60 AND overlap_score < 70") s60 = c.fetchone()[0] conn.close() except: pending = sent = rejected = total = s90 = s80 = s70 = s60 = 0 uptime = None if state.get('started_at'): try: start = datetime.fromisoformat(state['started_at']) if isinstance(state['started_at'], str) else state['started_at'] uptime = int((datetime.now() - start).total_seconds()) except: pass self._send_json({ 'running': state.get('running', False), 'dry_run': state.get('dry_run', False), 'uptime_seconds': uptime, 'intros_today': state.get('intros_today', 0), 'matches_pending': pending, 'matches_sent': sent, 'matches_rejected': rejected, 'matches_total': total, 'score_90_plus': s90, 'score_80_89': s80, 'score_70_79': s70, 'score_60_69': s60, }) def _handle_your_matches(self): """matches involving the host - shows both directions""" import sqlite3 import json as j from db.users import get_priority_users limit = 15 if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('limit='): try: limit = int(p.split('=')[1]) except: pass try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'matches': [], 'host': None}) db.close() return host = users[0] host_name = host.get('github') or host.get('name') conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT m.id, m.overlap_score, m.overlap_reasons, m.status, h1.username, h1.platform, h1.contact, h2.username, h2.platform, h2.contact FROM matches m JOIN humans h1 ON m.human_a_id = h1.id JOIN humans h2 ON m.human_b_id = h2.id WHERE (h1.username = ? OR h2.username = ?) AND m.status = 'pending' AND m.overlap_score >= 60 ORDER BY m.overlap_score DESC LIMIT ?""", (host_name, host_name, limit)) matches = [] for row in c.fetchall(): if row[4] == host_name: other_user, other_platform = row[7], row[8] other_contact = j.loads(row[9]) if row[9] else {} else: other_user, other_platform = row[4], row[5] other_contact = j.loads(row[6]) if row[6] else {} reasons = j.loads(row[2]) if row[2] else [] matches.append({ 'id': row[0], 'score': int(row[1]), 'reasons': reasons, 'status': row[3], 'other_user': other_user, 'other_platform': other_platform, 'contact': other_contact.get('email') or other_contact.get('mastodon') or '' }) conn.close() db.close() self._send_json({'host': host_name, 'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_preview_match_draft(self): """preview draft for a match - dir=to_you or to_them""" import sqlite3 import json as j from introd.groq_draft import draft_intro_with_llm from db.users import get_priority_users match_id = None direction = 'to_you' if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('id='): try: match_id = int(p.split('=')[1]) except: pass if p.startswith('dir='): direction = p.split('=')[1] if not match_id: self._send_json({'error': 'need ?id=match_id'}, 400) return cache_key = f"{match_id}_{direction}" cached = get_cached_draft(cache_key, 'match') if cached: cached['cached'] = True self._send_json(cached) return try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'error': 'no priority user'}, 404) db.close() return host = users[0] host_name = host.get('github') or host.get('name') conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT h1.username, h1.platform, h1.contact, h1.extra, h2.username, h2.platform, h2.contact, h2.extra, m.overlap_score, m.overlap_reasons FROM matches m JOIN humans h1 ON m.human_a_id = h1.id JOIN humans h2 ON m.human_b_id = h2.id WHERE m.id = ?""", (match_id,)) row = c.fetchone() conn.close() db.close() if not row: self._send_json({'error': 'match not found'}, 404) return human_a = {'username': row[0], 'platform': row[1], 'contact': j.loads(row[2]) if row[2] else {}, 'extra': j.loads(row[3]) if row[3] else {}} human_b = {'username': row[4], 'platform': row[5], 'contact': j.loads(row[6]) if row[6] else {}, 'extra': j.loads(row[7]) if row[7] else {}} reasons = j.loads(row[9]) if row[9] else [] if human_a['username'] == host_name: host_human, other_human = human_a, human_b else: host_human, other_human = human_b, human_a if direction == 'to_you': match_data = {'human_a': host_human, 'human_b': other_human, 'overlap_score': row[8], 'overlap_reasons': reasons} recipient_name = host_name about_name = other_human['username'] else: match_data = {'human_a': other_human, 'human_b': host_human, 'overlap_score': row[8], 'overlap_reasons': reasons} recipient_name = other_human['username'] about_name = host_name result, error = draft_intro_with_llm(match_data, recipient='a', dry_run=True) if error: self._send_json({'error': error}, 500) return response = { 'match_id': match_id, 'direction': direction, 'to': recipient_name, 'about': about_name, 'subject': result.get('subject'), 'draft': result.get('draft_html'), 'score': row[8], 'cached': False, } cache_draft(cache_key, response, 'match') self._send_json(response) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_host_matches(self): """matches for priority user""" import sqlite3 import json as j from db.users import get_priority_users limit = 20 if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('limit='): try: limit = int(p.split('=')[1]) except: pass try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'matches': [], 'host': None}) db.close() return host = users[0] conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT pm.id, pm.overlap_score, pm.overlap_reasons, pm.status, h.username, h.platform, h.contact FROM priority_matches pm JOIN humans h ON pm.matched_human_id = h.id WHERE pm.priority_user_id = ? ORDER BY pm.overlap_score DESC LIMIT ?""", (host['id'], limit)) matches = [] for row in c.fetchall(): reasons = j.loads(row[2]) if row[2] else [] contact = j.loads(row[6]) if row[6] else {} matches.append({'id': row[0], 'score': int(row[1]), 'reasons': reasons, 'status': row[3], 'other_user': row[4], 'other_platform': row[5], 'contact': contact.get('email') or contact.get('mastodon') or contact.get('github') or ''}) conn.close() db.close() self._send_json({'host': host.get('github') or host.get('name'), 'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_preview_host_draft(self): """preview draft for a priority match - dir=to_you or to_them""" import sqlite3 import json as j from introd.groq_draft import draft_intro_with_llm from db.users import get_priority_users match_id = None direction = 'to_you' if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('id='): try: match_id = int(p.split('=')[1]) except: pass if p.startswith('dir='): direction = p.split('=')[1] if not match_id: self._send_json({'error': 'need ?id=match_id'}, 400) return cache_key = f"host_{match_id}_{direction}" cached = get_cached_draft(cache_key, 'host') if cached: cached['cached'] = True self._send_json(cached) return try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'error': 'no priority user'}, 404) db.close() return host = users[0] conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() # Get the matched human from priority_matches c.execute("""SELECT h.username, h.platform, h.contact, h.extra, pm.overlap_score, pm.overlap_reasons, h.bio FROM priority_matches pm JOIN humans h ON pm.matched_human_id = h.id WHERE pm.id = ?""", (match_id,)) row = c.fetchone() conn.close() db.close() if not row: self._send_json({'error': 'match not found'}, 404) return # The matched person (who we found for the host) other = {'username': row[0], 'platform': row[1], 'bio': row[6], 'contact': j.loads(row[2]) if row[2] else {}, 'extra': j.loads(row[3]) if row[3] else {}} # Build host as human_a (recipient), other as human_b (subject) host_human = {'username': host.get('github') or host.get('name'), 'platform': 'priority', 'contact': {'email': host.get('email'), 'mastodon': host.get('mastodon'), 'github': host.get('github')}, 'extra': {'bio': host.get('bio'), 'interests': host.get('interests')}} reasons = j.loads(row[5]) if row[5] else [] match_data = {'human_a': host_human, 'human_b': other, 'overlap_score': row[4], 'overlap_reasons': reasons} # direction determines who gets the intro if direction == 'to_you': # intro TO host ABOUT other match_data = {'human_a': host_human, 'human_b': other, 'overlap_score': row[4], 'overlap_reasons': reasons} to_name = host.get('github') or host.get('name') about_name = other['username'] else: # intro TO other ABOUT host match_data = {'human_a': other, 'human_b': host_human, 'overlap_score': row[4], 'overlap_reasons': reasons} to_name = other['username'] about_name = host.get('github') or host.get('name') result, error = draft_intro_with_llm(match_data, recipient='a', dry_run=True) if error: self._send_json({'error': error}, 500) return cache_key = f"host_{match_id}_{direction}" response = { 'match_id': match_id, 'direction': direction, 'to': to_name, 'about': about_name, 'subject': result.get('subject'), 'draft': result.get('draft_html'), 'score': row[4], 'cached': False, } cache_draft(cache_key, response, 'host') self._send_json(response) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_preview_draft(self): import sqlite3 import json as j from introd.groq_draft import draft_intro_with_llm match_id = None if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('id='): try: match_id = int(p.split('=')[1]) except: pass if not match_id: self._send_json({'error': 'need ?id=match_id'}, 400) return # check cache first cached = get_cached_draft(match_id, 'queue') if cached: cached['cached'] = True self._send_json(cached) return try: conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT h1.username, h1.platform, h1.contact, h1.extra, h2.username, h2.platform, h2.contact, h2.extra, m.overlap_score, m.overlap_reasons FROM matches m JOIN humans h1 ON m.human_a_id = h1.id JOIN humans h2 ON m.human_b_id = h2.id WHERE m.id = ?""", (match_id,)) row = c.fetchone() conn.close() if not row: self._send_json({'error': 'match not found'}, 404) return human_a = {'username': row[0], 'platform': row[1], 'contact': j.loads(row[2]) if row[2] else {}, 'extra': j.loads(row[3]) if row[3] else {}} human_b = {'username': row[4], 'platform': row[5], 'contact': j.loads(row[6]) if row[6] else {}, 'extra': j.loads(row[7]) if row[7] else {}} reasons = j.loads(row[9]) if row[9] else [] match_data = {'human_a': human_a, 'human_b': human_b, 'overlap_score': row[8], 'overlap_reasons': reasons} result, error = draft_intro_with_llm(match_data, recipient='a', dry_run=True) if error: self._send_json({'error': error}, 500) return response = { 'match_id': match_id, 'to': human_a['username'], 'about': human_b['username'], 'subject': result.get('subject'), 'draft': result.get('draft_html'), 'score': row[8], 'cached': False, } cache_draft(match_id, response, 'queue') self._send_json(response) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_pending_about_you(self): """pending intros where host is human_b (being introduced to others)""" import sqlite3 import json as j from db.users import get_priority_users limit = 10 if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('limit='): try: limit = int(p.split('=')[1]) except: pass try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'matches': []}) db.close() return host = users[0] host_name = host.get('github') or host.get('name') conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT m.id, h1.username, h1.platform, h1.contact, m.overlap_score, m.overlap_reasons FROM matches m JOIN humans h1 ON m.human_a_id = h1.id JOIN humans h2 ON m.human_b_id = h2.id WHERE h2.username = ? AND m.status = 'pending' AND m.overlap_score >= 60 ORDER BY m.overlap_score DESC LIMIT ?""", (host_name, limit)) matches = [] for row in c.fetchall(): contact = j.loads(row[3]) if row[3] else {} reasons = j.loads(row[5]) if row[5] else [] method = 'email' if contact.get('email') else ('mastodon' if contact.get('mastodon') else None) matches.append({'id': row[0], 'to_user': row[1], 'to_platform': row[2], 'score': int(row[4]), 'reasons': reasons[:3], 'method': method, 'contact': contact.get('email') or contact.get('mastodon') or ''}) conn.close() db.close() self._send_json({'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_pending_to_you(self): """pending intros where host is human_a (receiving intro about others)""" import sqlite3 import json as j from db.users import get_priority_users limit = 20 if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('limit='): try: limit = int(p.split('=')[1]) except: pass try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({'matches': []}) db.close() return host = users[0] conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT pm.id, h.username, h.platform, pm.overlap_score, pm.overlap_reasons FROM priority_matches pm JOIN humans h ON pm.matched_human_id = h.id WHERE pm.priority_user_id = ? AND pm.status IN ('new', 'pending') ORDER BY pm.overlap_score DESC LIMIT ?""", (host['id'], limit)) matches = [] for row in c.fetchall(): reasons = j.loads(row[4]) if row[4] else [] matches.append({'id': row[0], 'about_user': row[1], 'about_platform': row[2], 'score': int(row[3]), 'reasons': reasons[:3]}) conn.close() db.close() self._send_json({'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_pending_matches(self): """pending matches - returns BOTH directions for each match""" import sqlite3 import json as j limit = 30 if '?' in self.path: for p in self.path.split('?')[1].split('&'): if p.startswith('limit='): try: limit = int(p.split('=')[1]) except: pass try: conn = sqlite3.connect('/data/db/connectd.db') c = conn.cursor() c.execute("""SELECT m.id, h1.username, h1.platform, h1.contact, h2.username, h2.platform, h2.contact, m.overlap_score, m.overlap_reasons FROM matches m JOIN humans h1 ON m.human_a_id = h1.id JOIN humans h2 ON m.human_b_id = h2.id WHERE m.status = 'pending' AND m.overlap_score >= 60 ORDER BY m.overlap_score DESC LIMIT ?""", (limit // 2,)) matches = [] for row in c.fetchall(): contact_a = j.loads(row[3]) if row[3] else {} contact_b = j.loads(row[6]) if row[6] else {} reasons = j.loads(row[8]) if row[8] else [] # direction 1: TO human_a ABOUT human_b method_a = 'email' if contact_a.get('email') else ('mastodon' if contact_a.get('mastodon') else None) matches.append({'id': row[0], 'to_user': row[1], 'about_user': row[4], 'score': int(row[7]), 'reasons': reasons[:3], 'method': method_a, 'contact': contact_a.get('email') or contact_a.get('mastodon') or ''}) # direction 2: TO human_b ABOUT human_a method_b = 'email' if contact_b.get('email') else ('mastodon' if contact_b.get('mastodon') else None) matches.append({'id': row[0], 'to_user': row[4], 'about_user': row[1], 'score': int(row[7]), 'reasons': reasons[:3], 'method': method_b, 'contact': contact_b.get('email') or contact_b.get('mastodon') or ''}) conn.close() self._send_json({'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_stats(self): """return database statistics (local + central)""" try: db = Database() stats = db.stats() db.close() # add central API stats if configured if CENTRAL_API and CENTRAL_KEY: try: headers = {'X-API-Key': CENTRAL_KEY} resp = requests.get(f'{CENTRAL_API}/stats', headers=headers, timeout=5) if resp.status_code == 200: central = resp.json() stats['central'] = { 'total_humans': central.get('total_humans', 0), 'lost_builders': central.get('lost_builders', 0), 'builders': central.get('builders', 0), 'total_matches': central.get('total_matches', 0), 'intros_sent': central.get('intros_sent', 0), 'active_instances': central.get('active_instances', 0), } except Exception as ce: stats['central'] = {'error': str(ce)} self._send_json(stats) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_health(self): """return daemon health status""" state = get_daemon_state() health = { 'status': 'running' if state['running'] else 'stopped', 'dry_run': state['dry_run'], 'uptime_seconds': None, } if state['started_at']: uptime = datetime.now() - datetime.fromisoformat(state['started_at']) health['uptime_seconds'] = int(uptime.total_seconds()) self._send_json(health) def _handle_state(self): """return full daemon state""" state = get_daemon_state() # convert datetimes to strings for key in ['last_scout', 'last_match', 'last_intro', 'last_lost', 'started_at']: if state[key] and isinstance(state[key], datetime): state[key] = state[key].isoformat() self._send_json(state) def _handle_priority_matches(self): """return priority matches for HA sensor""" try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({ 'count': 0, 'new_count': 0, 'top_matches': [], }) db.close() return # get matches for first priority user (host) user = users[0] matches = get_priority_user_matches(db.conn, user['id'], limit=10) new_count = sum(1 for m in matches if m.get('status') == 'new') top_matches = [] for m in matches[:5]: overlap_reasons = m.get('overlap_reasons', '[]') if isinstance(overlap_reasons, str): import json as json_mod overlap_reasons = json_mod.loads(overlap_reasons) if overlap_reasons else [] top_matches.append({ 'username': m.get('username'), 'platform': m.get('platform'), 'score': m.get('score', 0), 'overlap_score': m.get('overlap_score', 0), 'reasons': overlap_reasons[:3], 'url': m.get('url'), 'status': m.get('status', 'new'), }) db.close() self._send_json({ 'count': len(matches), 'new_count': new_count, 'top_matches': top_matches, }) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_top_humans(self): """return top scoring humans for HA sensor""" try: db = Database() humans = db.get_all_humans(min_score=50, limit=5) top_humans = [] for h in humans: contact = h.get('contact', '{}') if isinstance(contact, str): import json as json_mod contact = json_mod.loads(contact) if contact else {} signals = h.get('signals', '[]') if isinstance(signals, str): import json as json_mod signals = json_mod.loads(signals) if signals else [] top_humans.append({ 'username': h.get('username'), 'platform': h.get('platform'), 'score': h.get('score', 0), 'name': h.get('name'), 'signals': signals[:5], 'contact_method': 'email' if contact.get('email') else 'mastodon' if contact.get('mastodon') else 'matrix' if contact.get('matrix') else 'manual', }) db.close() self._send_json({ 'count': len(humans), 'top_humans': top_humans, }) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_user(self): """return priority user info for HA sensor""" try: db = Database() users = get_priority_users(db.conn) if not users: self._send_json({ 'configured': False, 'score': 0, 'signals': [], 'match_count': 0, }) db.close() return user = users[0] signals = user.get('signals', '[]') if isinstance(signals, str): import json as json_mod signals = json_mod.loads(signals) if signals else [] interests = user.get('interests', '[]') if isinstance(interests, str): import json as json_mod interests = json_mod.loads(interests) if interests else [] matches = get_priority_user_matches(db.conn, user['id'], limit=100) db.close() self._send_json({ 'configured': True, 'name': user.get('name'), 'github': user.get('github'), 'mastodon': user.get('mastodon'), 'reddit': user.get('reddit'), 'lobsters': user.get('lobsters'), 'matrix': user.get('matrix'), 'lemmy': user.get('lemmy'), 'discord': user.get('discord'), 'bluesky': user.get('bluesky'), 'score': user.get('score', 0), 'signals': signals[:10], 'interests': interests, 'location': user.get('location'), 'bio': user.get('bio'), 'match_count': len(matches), 'new_match_count': sum(1 for m in matches if m.get('status') == 'new'), }) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_lost_builders(self): """return lost builders with their inspiring matches""" try: from matchd.lost import find_matches_for_lost_builders db = Database() matches, error = find_matches_for_lost_builders(db, min_lost_score=30, min_values_score=15, limit=50) result = { 'total': len(matches) if matches else 0, 'error': error, 'matches': [] } if matches: for m in matches: lost = m.get('lost_user', {}) builder = m.get('inspiring_builder', {}) result['matches'].append({ 'lost_user': lost.get('username'), 'lost_platform': lost.get('platform'), 'lost_score': lost.get('lost_potential_score', 0), 'values_score': lost.get('score', 0), 'builder': builder.get('username'), 'builder_platform': builder.get('platform'), 'builder_score': builder.get('score', 0), 'builder_repos': m.get('builder_repos', 0), 'builder_stars': m.get('builder_stars', 0), 'match_score': m.get('match_score', 0), 'shared': m.get('shared_interests', [])[:5], }) db.close() self._send_json(result) except Exception as e: self._send_json({'error': str(e)}, 500) def run_api_server(): """run the API server in a thread""" server = HTTPServer(('0.0.0.0', API_PORT), APIHandler) print(f"connectd api running on port {API_PORT}") server.serve_forever() def start_api_thread(): """start API server in background thread""" thread = threading.Thread(target=run_api_server, daemon=True) thread.start() return thread if __name__ == '__main__': # standalone mode for testing print(f"starting connectd api on port {API_PORT}...") run_api_server()