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:
238
server.py
238
server.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user