#!/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 central_client import CentralClient, get_client from profile_page import render_profile # central API config import requests CENTRAL_API = os.environ.get('CONNECTD_CENTRAL_API', '') CENTRAL_KEY = os.environ.get('CONNECTD_API_KEY', '') # global client instance _central = None def get_central(): global _central if _central is None: _central = get_client() return _central 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() elif path.startswith('/profile/'): self._handle_profile_by_username() elif path.startswith('/humans/') and not path.startswith('/api/'): self._handle_profile_by_id() elif path.startswith('/api/humans/') and path.endswith('/full'): self._handle_human_full_json() 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): try: central = get_central() history = central.get_outreach_history(status='sent', limit=20) sent = [{'recipient_id': h.get('human_id'), 'method': h.get('sent_via', 'unknown'), 'draft': h.get('draft', ''), 'timestamp': h.get('completed_at', '')} for h in history] self._send_json({"sent": sent}) except Exception as e: self._send_json({"sent": [], "error": str(e)}) def _handle_failed_intros(self): try: central = get_central() history = central.get_outreach_history(status='failed', limit=50) failed = [{'recipient_id': h.get('human_id'), 'error': h.get('error', 'unknown'), 'timestamp': h.get('completed_at', '')} for h in history] self._send_json({"failed": failed}) except Exception as e: self._send_json({"failed": [], "error": str(e)}) def _handle_host(self): """daemon status and match stats from central""" state = get_daemon_state() try: central = get_central() stats = central.get_stats() total = stats.get('total_matches', 0) sent = stats.get('intros_sent', 0) pending = stats.get('pending_outreach', 0) # score distribution - sample high scores only high_matches = central.get_matches(min_score=60, limit=5000) s90 = sum(1 for m in high_matches if m.get('overlap_score', 0) >= 90) s80 = sum(1 for m in high_matches if 80 <= m.get('overlap_score', 0) < 90) s70 = sum(1 for m in high_matches if 70 <= m.get('overlap_score', 0) < 80) s60 = sum(1 for m in high_matches if 60 <= m.get('overlap_score', 0) < 70) except: pending = sent = 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': 0, '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): """top matches from central""" import json as j 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: central = get_central() raw_matches = central.get_matches(min_score=60, limit=limit) matches = [] for m in raw_matches: reasons = j.loads(m.get('overlap_reasons') or '[]') if isinstance(m.get('overlap_reasons'), str) else (m.get('overlap_reasons') or []) matches.append({ 'id': m.get('id'), 'score': int(m.get('overlap_score', 0)), 'reasons': reasons[:3] if isinstance(reasons, list) else [], 'status': 'pending', 'other_user': m.get('human_b_username'), 'other_platform': m.get('human_b_platform'), 'contact': '' }) self._send_json({'host': 'central', '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 from central - returns BOTH directions for each match""" 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: central = get_central() raw_matches = central.get_matches(min_score=60, limit=limit // 2) matches = [] for m in raw_matches: reasons = j.loads(m.get('overlap_reasons') or '[]') if isinstance(m.get('overlap_reasons'), str) else (m.get('overlap_reasons') or []) # direction 1: TO human_a ABOUT human_b matches.append({ 'id': m.get('id'), 'to_user': m.get('human_a_username'), 'about_user': m.get('human_b_username'), 'score': int(m.get('overlap_score', 0)), 'reasons': reasons[:3] if isinstance(reasons, list) else [], 'method': 'pending', 'contact': '' }) # direction 2: TO human_b ABOUT human_a matches.append({ 'id': m.get('id'), 'to_user': m.get('human_b_username'), 'about_user': m.get('human_a_username'), 'score': int(m.get('overlap_score', 0)), 'reasons': reasons[:3] if isinstance(reasons, list) else [], 'method': 'pending', 'contact': '' }) self._send_json({'matches': matches}) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_stats(self): """return database statistics from central""" try: central = get_central() stats = central.get_stats() 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 from central""" try: central = get_central() lost = central.get_lost_builders(min_score=30, limit=50) builders = central.get_builders(min_score=50, limit=20) # simple matching: pair lost builders with builders who have similar signals result = { 'total': len(lost), 'error': None if builders else 'no active builders available', 'matches': [] } if lost and builders: for l in lost[:50]: # find best matching builder lost_signals = set(json.loads(l.get('signals') or '[]')) best_builder = None best_score = 0 for b in builders: builder_signals = set(json.loads(b.get('signals') or '[]')) shared = lost_signals & builder_signals score = len(shared) * 10 + b.get('score', 0) * 0.1 if score > best_score: best_score = score best_builder = b best_shared = list(shared) if best_builder: extra = json.loads(best_builder.get('extra') or '{}') result['matches'].append({ 'lost_user': l.get('username'), 'lost_platform': l.get('platform'), 'lost_score': l.get('lost_potential_score', 0), 'values_score': l.get('score', 0), 'builder': best_builder.get('username'), 'builder_platform': best_builder.get('platform'), 'builder_score': best_builder.get('score', 0), 'builder_repos': extra.get('public_repos', 0), 'builder_stars': extra.get('stars', 0), 'match_score': best_score, 'shared': best_shared[:5], }) self._send_json(result) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_profile_by_username(self): """render profile page by username""" path = self.path.split('?')[0] parts = path.strip('/').split('/') if len(parts) == 2: username = parts[1] platform = None elif len(parts) == 3: platform = parts[1] username = parts[2] else: self._send_json({'error': 'invalid path'}, 400) return try: central = get_central() if platform: humans = central.get_humans(platform=platform, limit=100) human = next((h for h in humans if h.get('username', '').lower() == username.lower()), None) else: human = None for plat in ['github', 'reddit', 'mastodon', 'lobsters', 'lemmy']: humans = central.get_humans(platform=plat, limit=500) human = next((h for h in humans if h.get('username', '').lower() == username.lower()), None) if human: break if not human: self.send_response(404) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write(f'

not found

no human found with username: {username}

back'.encode()) return matches = central.get_matches(limit=10000) match_count = sum(1 for m in matches if m.get('human_a_id') == human['id'] or m.get('human_b_id') == human['id']) html = render_profile(human, match_count=match_count) self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write(html.encode()) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_profile_by_id(self): """render profile page by human ID""" path = self.path.split('?')[0] parts = path.strip('/').split('/') if len(parts) != 2: self._send_json({'error': 'invalid path'}, 400) return try: human_id = int(parts[1]) except ValueError: self._send_json({'error': 'invalid id'}, 400) return try: central = get_central() human = central.get_human(human_id) if not human: self.send_response(404) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write(f'

not found

no human found with id: {human_id}

back'.encode()) return matches = central.get_matches(limit=10000) match_count = sum(1 for m in matches if m.get('human_a_id') == human['id'] or m.get('human_b_id') == human['id']) html = render_profile(human, match_count=match_count) self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write(html.encode()) except Exception as e: self._send_json({'error': str(e)}, 500) def _handle_human_full_json(self): """return full human data as JSON""" path = self.path.split('?')[0] parts = path.strip('/').split('/') if len(parts) != 4 or parts[0] != 'api' or parts[1] != 'humans' or parts[3] != 'full': self._send_json({'error': 'invalid path'}, 400) return try: human_id = int(parts[2]) except ValueError: self._send_json({'error': 'invalid id'}, 400) return try: central = get_central() human = central.get_human(human_id) if not human: self._send_json({'error': 'not found'}, 404) return for field in ['signals', 'negative_signals', 'reasons', 'contact', 'extra']: if field in human and isinstance(human[field], str): try: human[field] = json.loads(human[field]) except: pass matches = central.get_matches(limit=10000) human['match_count'] = sum(1 for m in matches if m.get('human_a_id') == human['id'] or m.get('human_b_id') == human['id']) self._send_json(human) 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()