Skip to content

Commit 107e2f7

Browse files
committed
feat: add peer connections system with requests, accept/decline, and dashboard UI
1 parent de67d5e commit 107e2f7

3 files changed

Lines changed: 238 additions & 2 deletions

File tree

public/dashboard.html

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>Dashboard - Alpha One Labs</title>
77
<link rel="icon" type="image/png" href="/images/logo.png" />
8-
<script src="https://cdn.tailwindcss.com"></script>
8+
<script src="https://cdn.tailwindcss.com">
9+
</script>
910
<script>tailwind.config = { theme: { extend: { colors: { brand: { DEFAULT:'#4F46E5', dark:'#3730A3' } } } } };</script>
1011
<style>
1112
.hidden { display: none !important; }
@@ -57,6 +58,36 @@ <h2 class="text-xl font-bold text-slate-800 mb-5" id="section-title">My Activiti
5758
<a id="empty-action-link" href="/" class="mt-4 inline-block text-brand font-semibold hover:underline" id="empty-action-text">Browse activities</a>
5859
</div>
5960
<div id="joined-section"></div>
61+
<!-- Peer Connections Section -->
62+
<div class="mt-8 bg-white rounded-2xl shadow-sm border border-slate-100 p-6" id="peers-section">
63+
<div class="flex items-center justify-between mb-4">
64+
<h2 class="font-bold text-slate-800 text-lg">&#128101; Peer Connections</h2>
65+
<span id="peer-requests-badge" class="hidden bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full"></span>
66+
</div>
67+
68+
<!-- Tabs -->
69+
<div class="flex gap-2 mb-4 border-b border-slate-100">
70+
<button type="button" role="tab" aria-selected="true" onclick="showPeerTab('connections')" id="tab-connections" class="peer-tab px-4 py-2 text-sm font-semibold text-indigo-600 border-b-2 border-indigo-600">My Peers</button>
71+
<button type="button" role="tab" aria-selected="false" onclick="showPeerTab('requests')" id="tab-requests" class="peer-tab px-4 py-2 text-sm font-semibold text-slate-400 border-b-2 border-transparent">Requests</button>
72+
<button type="button" role="tab" aria-selected="false" onclick="showPeerTab('find')" id="tab-find" class="peer-tab px-4 py-2 text-sm font-semibold text-slate-400 border-b-2 border-transparent">Find Peers</button>
73+
</div>
74+
75+
<!-- My Connections -->
76+
<div id="peers-connections" class="peer-panel">
77+
<div id="peers-list" class="space-y-3"></div>
78+
</div>
79+
80+
<!-- Incoming Requests -->
81+
<div id="peers-requests" class="peer-panel hidden">
82+
<div id="requests-list" class="space-y-3"></div>
83+
</div>
84+
85+
<!-- Find Peers -->
86+
<div id="peers-find" class="peer-panel hidden">
87+
<p class="text-sm text-slate-500 mb-3">Connect with other learners from your activities.</p>
88+
<div id="suggestions-list" class="space-y-3"></div>
89+
</div>
90+
</div>
6091
</main>
6192

6293
<footer class="bg-slate-800 text-slate-400 py-6 text-center text-sm mt-10">
@@ -174,5 +205,6 @@ <h2 class="text-xl font-bold text-slate-800 mb-5" id="section-title">My Activiti
174205
'<p class="col-span-3 text-red-400 text-sm">Failed to load dashboard: ' + e.message + '</p>';
175206
});
176207
</script>
208+
177209
</body>
178210
</html>

schema.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,20 @@ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(activity_id);
9393
CREATE INDEX IF NOT EXISTS idx_sa_session ON session_attendance(session_id);
9494
CREATE INDEX IF NOT EXISTS idx_sa_user ON session_attendance(user_id);
9595
CREATE INDEX IF NOT EXISTS idx_at_activity ON activity_tags(activity_id);
96+
97+
-- PEER CONNECTIONS
98+
CREATE TABLE IF NOT EXISTS peer_connections (
99+
id TEXT PRIMARY KEY,
100+
requester_id TEXT NOT NULL,
101+
addressee_id TEXT NOT NULL,
102+
status TEXT NOT NULL DEFAULT 'pending',
103+
message TEXT,
104+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
105+
updated_at TEXT,
106+
UNIQUE (requester_id, addressee_id),
107+
FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE,
108+
FOREIGN KEY (addressee_id) REFERENCES users(id) ON DELETE CASCADE
109+
);
110+
CREATE INDEX IF NOT EXISTS idx_pc_requester ON peer_connections(requester_id);
111+
CREATE INDEX IF NOT EXISTS idx_pc_addressee ON peer_connections(addressee_id);
112+
CREATE INDEX IF NOT EXISTS idx_pc_status ON peer_connections(status);

src/worker.py

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,23 @@ def _is_basic_auth_valid(req, env) -> bool:
450450
"CREATE INDEX IF NOT EXISTS idx_sa_session ON session_attendance(session_id)",
451451
"CREATE INDEX IF NOT EXISTS idx_sa_user ON session_attendance(user_id)",
452452
"CREATE INDEX IF NOT EXISTS idx_at_activity ON activity_tags(activity_id)",
453+
# Peer Connections
454+
"""CREATE TABLE IF NOT EXISTS peer_connections (
455+
id TEXT PRIMARY KEY,
456+
requester_id TEXT NOT NULL,
457+
addressee_id TEXT NOT NULL,
458+
status TEXT NOT NULL DEFAULT 'pending',
459+
message TEXT,
460+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
461+
updated_at TEXT,
462+
UNIQUE (requester_id, addressee_id),
463+
FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE,
464+
FOREIGN KEY (addressee_id) REFERENCES users(id) ON DELETE CASCADE
465+
)""",
466+
"CREATE INDEX IF NOT EXISTS idx_pc_requester ON peer_connections(requester_id)",
467+
"CREATE INDEX IF NOT EXISTS idx_pc_addressee ON peer_connections(addressee_id)",
468+
"CREATE INDEX IF NOT EXISTS idx_pc_status ON peer_connections(status)",
469+
453470
]
454471

455472

@@ -1291,6 +1308,25 @@ async def _dispatch(request, env):
12911308
if path == "/api/admin/table-counts" and method == "GET":
12921309
return await api_admin_table_counts(request, env)
12931310

1311+
1312+
# Peer Connections
1313+
if path == "/api/peers" and method == "GET":
1314+
return await api_list_peers(request, env)
1315+
if path == "/api/peers/requests" and method == "GET":
1316+
return await api_list_peer_requests(request, env)
1317+
m_pc1 = re.fullmatch(r"/api/peers/request/([A-Za-z0-9_-]+)", path)
1318+
if m_pc1 and method == "POST":
1319+
return await api_send_peer_request(request, env, m_pc1.group(1))
1320+
m_pc2 = re.fullmatch(r"/api/peers/([A-Za-z0-9_-]+)/accept", path)
1321+
if m_pc2 and method == "POST":
1322+
return await api_accept_peer_request(request, env, m_pc2.group(1))
1323+
m_pc3 = re.fullmatch(r"/api/peers/([A-Za-z0-9_-]+)/decline", path)
1324+
if m_pc3 and method == "POST":
1325+
return await api_decline_peer_request(request, env, m_pc3.group(1))
1326+
m_pc4 = re.fullmatch(r"/api/peers/([A-Za-z0-9_-]+)", path)
1327+
if m_pc4 and method == "DELETE":
1328+
return await api_remove_peer(request, env, m_pc4.group(1))
1329+
12941330
return err("API endpoint not found", 404)
12951331

12961332
return await serve_static(path, env)
@@ -1301,4 +1337,155 @@ async def on_fetch(request, env):
13011337
return await _dispatch(request, env)
13021338
except Exception as e:
13031339
capture_exception(e, request, env, "on_fetch_unhandled")
1304-
return err("Internal server error", 500)
1340+
return err("Internal server error", 500)
1341+
1342+
# ---------------------------------------------------------------------------
1343+
# Peer Connections API
1344+
# ---------------------------------------------------------------------------
1345+
1346+
async def api_list_peers(req, env):
1347+
"""GET /api/peers — list accepted connections."""
1348+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1349+
if not user:
1350+
return err("Authentication required", 401)
1351+
enc = env.ENCRYPTION_KEY
1352+
rows = await env.DB.prepare(
1353+
"SELECT pc.id, pc.created_at,"
1354+
" CASE WHEN pc.requester_id=? THEN pc.addressee_id ELSE pc.requester_id END AS peer_id,"
1355+
" u.name AS peer_name_enc"
1356+
" FROM peer_connections pc"
1357+
" JOIN users u ON u.id = CASE WHEN pc.requester_id=? THEN pc.addressee_id ELSE pc.requester_id END"
1358+
" WHERE (pc.requester_id=? OR pc.addressee_id=?) AND pc.status='accepted'"
1359+
" ORDER BY pc.created_at DESC"
1360+
).bind(user["id"], user["id"], user["id"], user["id"]).all()
1361+
peers = []
1362+
for r in rows.results or []:
1363+
peers.append({
1364+
"connection_id": r["id"],
1365+
"peer_id": r["peer_id"],
1366+
"peer_name": await decrypt_aes(r["peer_name_enc"] or "", enc),
1367+
"connected_at": r["created_at"],
1368+
})
1369+
return ok(peers)
1370+
1371+
1372+
async def api_list_peer_requests(req, env):
1373+
"""GET /api/peers/requests — list pending incoming requests."""
1374+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1375+
if not user:
1376+
return err("Authentication required", 401)
1377+
enc = env.ENCRYPTION_KEY
1378+
rows = await env.DB.prepare(
1379+
"SELECT pc.id, pc.message, pc.created_at, u.name AS requester_name_enc, pc.requester_id"
1380+
" FROM peer_connections pc"
1381+
" JOIN users u ON u.id = pc.requester_id"
1382+
" WHERE pc.addressee_id=? AND pc.status='pending'"
1383+
" ORDER BY pc.created_at DESC"
1384+
).bind(user["id"]).all()
1385+
requests = []
1386+
for r in rows.results or []:
1387+
requests.append({
1388+
"connection_id": r["id"],
1389+
"requester_id": r["requester_id"],
1390+
"requester_name": await decrypt_aes(r["requester_name_enc"] or "", enc),
1391+
"message": r["message"] or "",
1392+
"requested_at": r["created_at"],
1393+
})
1394+
return ok(requests)
1395+
1396+
1397+
async def api_send_peer_request(req, env, addressee_id: str):
1398+
"""POST /api/peers/request/:id — send a connection request."""
1399+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1400+
if not user:
1401+
return err("Authentication required", 401)
1402+
if user["id"] == addressee_id:
1403+
return err("You cannot connect with yourself", 400)
1404+
addressee = await env.DB.prepare("SELECT id FROM users WHERE id=?").bind(addressee_id).first()
1405+
if not addressee:
1406+
return err("User not found", 404)
1407+
existing = await env.DB.prepare(
1408+
"SELECT id, status FROM peer_connections"
1409+
" WHERE (requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)"
1410+
).bind(user["id"], addressee_id, addressee_id, user["id"]).first()
1411+
if existing:
1412+
if existing["status"] == "accepted":
1413+
return err("Already connected", 409)
1414+
if existing["status"] == "pending":
1415+
return err("Connection request already sent", 409)
1416+
if existing["status"] == "declined":
1417+
return err("Your previous request was declined", 409)
1418+
body, bad = await parse_json_object(req)
1419+
if bad:
1420+
return bad
1421+
raw_msg = body.get("message")
1422+
message = (raw_msg.strip()[:200] if isinstance(raw_msg, str) else "")
1423+
conn_id = new_id()
1424+
try:
1425+
await env.DB.prepare(
1426+
"INSERT INTO peer_connections (id, requester_id, addressee_id, message)"
1427+
" VALUES (?, ?, ?, ?)"
1428+
).bind(conn_id, user["id"], addressee_id, message).run()
1429+
except Exception as exc:
1430+
capture_exception(exc, req, env, "api_send_peer_request.insert")
1431+
return err("Failed to send request", 500)
1432+
return ok({"connection_id": conn_id}, "Connection request sent")
1433+
1434+
1435+
async def api_accept_peer_request(req, env, connection_id: str):
1436+
"""POST /api/peers/:id/accept — accept a pending request."""
1437+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1438+
if not user:
1439+
return err("Authentication required", 401)
1440+
pc = await env.DB.prepare(
1441+
"SELECT id, status FROM peer_connections WHERE id=? AND addressee_id=?"
1442+
).bind(connection_id, user["id"]).first()
1443+
if not pc:
1444+
return err("Connection request not found", 404)
1445+
if pc["status"] != "pending":
1446+
return err("Request is not pending", 409)
1447+
try:
1448+
await env.DB.prepare(
1449+
"UPDATE peer_connections SET status='accepted', updated_at=datetime('now') WHERE id=?"
1450+
).bind(connection_id).run()
1451+
except Exception as exc:
1452+
capture_exception(exc, req, env, "api_accept_peer_request.update")
1453+
return err("Failed to accept request", 500)
1454+
return ok(msg="Connection accepted")
1455+
1456+
1457+
async def api_decline_peer_request(req, env, connection_id: str):
1458+
"""POST /api/peers/:id/decline — decline a pending request."""
1459+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1460+
if not user:
1461+
return err("Authentication required", 401)
1462+
pc = await env.DB.prepare(
1463+
"SELECT id, status FROM peer_connections WHERE id=? AND addressee_id=?"
1464+
).bind(connection_id, user["id"]).first()
1465+
if not pc:
1466+
return err("Connection request not found", 404)
1467+
if pc["status"] != "pending":
1468+
return err("Request is not pending", 409)
1469+
try:
1470+
await env.DB.prepare(
1471+
"UPDATE peer_connections SET status='declined', updated_at=datetime('now') WHERE id=?"
1472+
).bind(connection_id).run()
1473+
except Exception as exc:
1474+
capture_exception(exc, req, env, "api_decline_peer_request.update")
1475+
return err("Failed to decline request", 500)
1476+
return ok(msg="Connection declined")
1477+
1478+
1479+
async def api_remove_peer(req, env, connection_id: str):
1480+
"""DELETE /api/peers/:id — remove an accepted connection."""
1481+
user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET)
1482+
if not user:
1483+
return err("Authentication required", 401)
1484+
pc = await env.DB.prepare(
1485+
"SELECT id FROM peer_connections"
1486+
" WHERE id=? AND (requester_id=? OR addressee_id=?) AND status='accepted'"
1487+
).bind(connection_id, user["id"], user["id"]).first()
1488+
if not pc:
1489+
return err("Connection not found", 404)
1490+
await env.DB.prepare("DELETE FROM peer_connections WHERE id=?").bind(connection_id).run()
1491+
return ok(msg="Connection removed")

0 commit comments

Comments
 (0)