Add uvicorn workers for better concurrency

Run with 4 workers to handle concurrent requests without blocking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-09 03:36:16 +00:00
parent 43a9a1cd64
commit 3756f5e630

238
server.py
View File

@@ -281,6 +281,7 @@ def base_html(title: str, content: str, username: str = None) -> str:
<a href="/assets" class="text-gray-400 hover:text-white transition-colors">Assets</a>
<a href="/activities" class="text-gray-400 hover:text-white transition-colors">Activities</a>
<a href="/users" class="text-gray-400 hover:text-white transition-colors">Users</a>
<a href="/anchors/ui" class="text-gray-400 hover:text-white transition-colors">Anchors</a>
</nav>
<main class="bg-dark-700 rounded-lg p-6">
@@ -2331,7 +2332,7 @@ async def get_object(content_hash: str, request: Request):
# ============ Anchoring (Bitcoin timestamps) ============
@app.post("/anchors/create")
async def create_anchor_endpoint(user: User = Depends(get_required_user)):
async def create_anchor_endpoint(request: Request):
"""
Create a new anchor for all unanchored activities.
@@ -2341,9 +2342,26 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)):
import anchoring
import ipfs_client
# Check auth (cookie or header)
username = get_user_from_cookie(request)
if not username:
if wants_html(request):
return HTMLResponse('''
<div class="p-3 bg-red-900/50 border border-red-700 rounded text-red-300">
<strong>Error:</strong> Login required
</div>
''')
raise HTTPException(401, "Authentication required")
# Get unanchored activities
unanchored = await db.get_unanchored_activities()
if not unanchored:
if wants_html(request):
return HTMLResponse('''
<div class="p-3 bg-yellow-900/50 border border-yellow-700 rounded text-yellow-300">
No unanchored activities to anchor.
</div>
''')
return {"message": "No unanchored activities", "anchored": 0}
activity_ids = [a["activity_id"] for a in unanchored]
@@ -2352,6 +2370,14 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)):
anchor = await anchoring.create_anchor(activity_ids, db, ipfs_client)
if anchor:
if wants_html(request):
return HTMLResponse(f'''
<div class="p-3 bg-green-900/50 border border-green-700 rounded text-green-300">
<strong>Success!</strong> Anchored {len(activity_ids)} activities.<br>
<span class="text-sm">Merkle root: {anchor["merkle_root"][:32]}...</span><br>
<span class="text-sm">Refresh page to see the new anchor.</span>
</div>
''')
return {
"message": f"Anchored {len(activity_ids)} activities",
"merkle_root": anchor["merkle_root"],
@@ -2359,6 +2385,12 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)):
"activity_count": anchor["activity_count"]
}
else:
if wants_html(request):
return HTMLResponse('''
<div class="p-3 bg-red-900/50 border border-red-700 rounded text-red-300">
<strong>Failed!</strong> Could not create anchor.
</div>
''')
raise HTTPException(500, "Failed to create anchor")
@@ -2462,6 +2494,208 @@ async def verify_activity_anchor(activity_id: str):
}
@app.post("/anchors/{merkle_root}/upgrade")
async def upgrade_anchor_proof(merkle_root: str):
"""
Try to upgrade an OTS proof from pending to confirmed.
Bitcoin confirmation typically takes 1-2 hours. Call this periodically
to check if the proof has been included in a Bitcoin block.
"""
import anchoring
import ipfs_client
import asyncio
anchor = await db.get_anchor(merkle_root)
if not anchor:
raise HTTPException(404, f"Anchor not found: {merkle_root}")
if anchor.get("confirmed_at"):
return {"status": "already_confirmed", "bitcoin_txid": anchor.get("bitcoin_txid")}
# Get current OTS proof from IPFS
ots_cid = anchor.get("ots_proof_cid")
if not ots_cid:
return {"status": "no_proof", "message": "No OTS proof stored"}
try:
ots_proof = await asyncio.to_thread(ipfs_client.get_bytes, ots_cid)
if not ots_proof:
return {"status": "error", "message": "Could not fetch OTS proof from IPFS"}
except Exception as e:
return {"status": "error", "message": f"IPFS error: {e}"}
# Try to upgrade
upgraded = await asyncio.to_thread(anchoring.upgrade_ots_proof, ots_proof)
if upgraded and len(upgraded) > len(ots_proof):
# Store upgraded proof on IPFS
try:
new_cid = await asyncio.to_thread(ipfs_client.add_bytes, upgraded)
# TODO: Update anchor record with new CID and confirmed status
return {
"status": "upgraded",
"message": "Proof upgraded - Bitcoin confirmation received",
"new_ots_cid": new_cid,
"proof_size": len(upgraded)
}
except Exception as e:
return {"status": "error", "message": f"Failed to store upgraded proof: {e}"}
else:
return {
"status": "pending",
"message": "Not yet confirmed on Bitcoin. Try again in ~1 hour.",
"proof_size": len(ots_proof) if ots_proof else 0
}
@app.get("/anchors/ui", response_class=HTMLResponse)
async def anchors_ui(request: Request):
"""Anchors UI page - view and test OpenTimestamps anchoring."""
username = get_user_from_cookie(request)
anchors = await db.get_all_anchors()
stats = await db.get_anchor_stats()
# Build anchors table rows
rows = ""
for anchor in anchors:
status = "confirmed" if anchor.get("confirmed_at") else "pending"
status_class = "text-green-400" if status == "confirmed" else "text-yellow-400"
merkle_root = anchor.get("merkle_root", "")[:16] + "..."
rows += f'''
<tr class="border-b border-dark-500 hover:bg-dark-600">
<td class="py-3 px-2 font-mono text-sm">{merkle_root}</td>
<td class="py-3 px-2">{anchor.get("activity_count", 0)}</td>
<td class="py-3 px-2 {status_class}">{status}</td>
<td class="py-3 px-2 text-sm">{format_date(anchor.get("created_at"), 16)}</td>
<td class="py-3 px-2">
<button
hx-post="/anchors/{anchor.get("merkle_root")}/upgrade"
hx-target="#upgrade-result-{anchor.get("merkle_root")[:8]}"
hx-swap="innerHTML"
class="px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm">
Check Status
</button>
<span id="upgrade-result-{anchor.get("merkle_root")[:8]}" class="ml-2 text-sm"></span>
</td>
</tr>
'''
if not rows:
rows = '<tr><td colspan="5" class="py-8 text-center text-gray-500">No anchors yet</td></tr>'
content = f'''
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<h2 class="text-xl font-semibold mb-4">Bitcoin Anchoring via OpenTimestamps</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-dark-600 rounded-lg p-4">
<div class="text-2xl font-bold text-blue-400">{stats.get("total_anchors", 0)}</div>
<div class="text-gray-400 text-sm">Total Anchors</div>
</div>
<div class="bg-dark-600 rounded-lg p-4">
<div class="text-2xl font-bold text-green-400">{stats.get("confirmed_anchors", 0)}</div>
<div class="text-gray-400 text-sm">Confirmed</div>
</div>
<div class="bg-dark-600 rounded-lg p-4">
<div class="text-2xl font-bold text-yellow-400">{stats.get("pending_anchors", 0)}</div>
<div class="text-gray-400 text-sm">Pending</div>
</div>
</div>
<div class="mb-6 p-4 bg-dark-600 rounded-lg">
<h3 class="font-semibold mb-2">Test Anchoring</h3>
<p class="text-sm text-gray-400 mb-3">Create a test anchor for unanchored activities, or test the OTS connection.</p>
<div class="flex gap-4">
<button
hx-post="/anchors/create"
hx-target="#test-result"
hx-swap="innerHTML"
class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded">
Create Anchor
</button>
<button
hx-post="/anchors/test-ots"
hx-target="#test-result"
hx-swap="innerHTML"
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded">
Test OTS Connection
</button>
</div>
<div id="test-result" class="mt-4 text-sm"></div>
</div>
<h3 class="font-semibold mb-3">Anchors</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-gray-400 border-b border-dark-500">
<th class="py-2 px-2">Merkle Root</th>
<th class="py-2 px-2">Activities</th>
<th class="py-2 px-2">Status</th>
<th class="py-2 px-2">Created</th>
<th class="py-2 px-2">Actions</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
<div class="mt-6 p-4 bg-dark-800 rounded-lg text-sm text-gray-400">
<h4 class="font-semibold text-gray-300 mb-2">How it works:</h4>
<ol class="list-decimal list-inside space-y-1">
<li>Activities are batched and hashed into a merkle tree</li>
<li>The merkle root is submitted to OpenTimestamps</li>
<li>OTS aggregates hashes and anchors to Bitcoin (~1-2 hours)</li>
<li>Once confirmed, anyone can verify the timestamp</li>
</ol>
</div>
'''
return HTMLResponse(base_html("Anchors", content, username))
@app.post("/anchors/test-ots", response_class=HTMLResponse)
async def test_ots_connection():
"""Test OpenTimestamps connection by submitting a test hash."""
import anchoring
import hashlib
import asyncio
# Create a test hash
test_data = f"test-{datetime.now(timezone.utc).isoformat()}"
test_hash = hashlib.sha256(test_data.encode()).hexdigest()
# Try to submit
try:
ots_proof = await asyncio.to_thread(anchoring.submit_to_opentimestamps, test_hash)
if ots_proof:
return HTMLResponse(f'''
<div class="p-3 bg-green-900/50 border border-green-700 rounded text-green-300">
<strong>Success!</strong> OpenTimestamps is working.<br>
<span class="text-sm">Test hash: {test_hash[:32]}...</span><br>
<span class="text-sm">Proof size: {len(ots_proof)} bytes</span>
</div>
''')
else:
return HTMLResponse('''
<div class="p-3 bg-red-900/50 border border-red-700 rounded text-red-300">
<strong>Failed!</strong> Could not reach OpenTimestamps servers.
</div>
''')
except Exception as e:
return HTMLResponse(f'''
<div class="p-3 bg-red-900/50 border border-red-700 rounded text-red-300">
<strong>Error:</strong> {str(e)}
</div>
''')
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8200)
uvicorn.run(app, host="0.0.0.0", port=8200, workers=4)