fieldwitness/frontends/web/templates/federation/status.html
Aaron D. Lee 2a6900abed
Some checks failed
CI / lint (push) Failing after 1m3s
CI / typecheck (push) Failing after 32s
Implement live gossip federation server (5 phases)
Phase 1: RFC 6962 consistency proofs in merkle.py
- Implemented _build_consistency_proof() with recursive subtree
  decomposition algorithm following RFC 6962 Section 2.1.2
- Added _subproof() recursive helper and _compute_root_of()
- Added standalone verify_consistency_proof() function

Phase 2: Federation API endpoints on FastAPI server
- GET /federation/status — merkle root + log size for gossip probes
- GET /federation/records?start=N&count=M — record fetch (cap 100)
- GET /federation/consistency-proof?old_size=N — Merkle proof
- POST /federation/records — accept records with trust filtering
  and SHA-256 deduplication
- Cached storage singleton for concurrent safety
- Added FEDERATION_DIR to paths.py

Phase 3: HttpTransport implementation
- Replaced stub with real aiohttp client (lazy import for optional dep)
- Reusable ClientSession with configurable timeout
- All 4 PeerTransport methods: get_status, get_records,
  get_consistency_proof, push_records
- FederationError wrapping for all network failures
- Added record_filter callback to GossipNode for trust-store filtering

Phase 4: Peer persistence (SQLite)
- New peer_store.py: SQLite-backed peer database + sync history
- Tables: peers (url, fingerprint, health, last_seen) and
  sync_history (timestamp, records_received, success/error)
- PeerStore follows dropbox.py SQLite pattern

Phase 5: CLI commands + Web UI dashboard
- CLI: federation status, peer-add, peer-remove, peer-list,
  sync-now (asyncio), history
- Flask blueprint at /federation/ with peer table, sync history,
  add/remove peer forms, local node info cards
- CSRF tokens on all forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:20:53 -04:00

109 lines
4.1 KiB
HTML

{% extends "base.html" %}
{% block title %}Federation — SooSeF{% endblock %}
{% block content %}
<h2><i class="bi bi-diagram-3 me-2"></i>Federation</h2>
<p class="text-muted">Gossip-based attestation sync between SooSeF instances.</p>
<div class="row mb-4">
<div class="col-md-4">
<div class="card bg-dark">
<div class="card-body">
<h6 class="card-subtitle text-muted">Local Node</h6>
<p class="mb-1">Records: <strong>{{ node_info.size }}</strong></p>
<p class="mb-0 text-muted small">Root: {{ node_info.root }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark">
<div class="card-body">
<h6 class="card-subtitle text-muted">Peers</h6>
<p class="mb-0"><strong>{{ peers|length }}</strong> configured</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark">
<div class="card-body">
<h6 class="card-subtitle text-muted">Last Sync</h6>
{% if history %}
<p class="mb-0">{{ history[0].synced_at[:16] }}</p>
{% else %}
<p class="mb-0 text-muted">Never</p>
{% endif %}
</div>
</div>
</div>
</div>
<h5>Peers</h5>
{% if peers %}
<table class="table table-dark table-sm">
<thead>
<tr><th>URL</th><th>Fingerprint</th><th>Records</th><th>Health</th><th>Last Seen</th><th></th></tr>
</thead>
<tbody>
{% for p in peers %}
<tr>
<td><code>{{ p.url }}</code></td>
<td><code>{{ p.fingerprint[:16] }}...</code></td>
<td>{{ p.last_size }}</td>
<td>{% if p.healthy %}<span class="text-success">OK</span>{% else %}<span class="text-danger">DOWN</span>{% endif %}</td>
<td>{{ p.last_seen.strftime('%Y-%m-%d %H:%M') if p.last_seen else 'Never' }}</td>
<td>
<form method="POST" action="{{ url_for('federation.peer_remove') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="url" value="{{ p.url }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Remove">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No peers configured. Add one below.</p>
{% endif %}
<div class="card bg-dark mb-4">
<div class="card-body">
<h6 class="card-title">Add Peer</h6>
<form method="POST" action="{{ url_for('federation.peer_add') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="row g-2">
<div class="col-md-5">
<input type="url" name="url" class="form-control bg-dark text-light" placeholder="https://peer:8000" required>
</div>
<div class="col-md-5">
<input type="text" name="fingerprint" class="form-control bg-dark text-light" placeholder="Ed25519 fingerprint" required>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Add</button>
</div>
</div>
</form>
</div>
</div>
{% if history %}
<h5>Sync History</h5>
<table class="table table-dark table-sm">
<thead>
<tr><th>Time</th><th>Peer</th><th>Records</th><th>Status</th></tr>
</thead>
<tbody>
{% for h in history %}
<tr>
<td>{{ h.synced_at[:19] }}</td>
<td><code>{{ h.peer_url[:30] }}</code></td>
<td>+{{ h.records_received }}</td>
<td>{% if h.success %}<span class="text-success">OK</span>{% else %}<span class="text-danger">{{ h.error[:40] if h.error else 'Failed' }}</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}