"""
# 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()