feat(appstore): federated Mesh Catalog over the annuaire directory — 0.3.0 (ref #768)

This commit is contained in:
CyberMind-FR 2026-07-01 07:44:35 +02:00
parent 9ee985bdb1
commit 8fdd95aa55
3 changed files with 99 additions and 0 deletions

View File

@ -156,6 +156,63 @@ async def catalog(category: Optional[str] = None, tier: Optional[str] = None,
return {"modules": items, "count": len(items), "total": len(st), "board_tier": board_tier()}
# ── Mesh catalog — federated view over the annuaire directory (#768, phase 2) ──
# Every node holds the converged gondwana directory. We read it read-only (the
# journal is owned by `secubox`, our own user) and join each ServiceOffer to the
# publishing node's NodeRecord, so the appstore shows a fleet-wide catalog:
# which service is offered/emancipated, by which node, over which channel, and
# how to reach it. Graceful no-op when secubox-annuaire is not installed.
ANNUAIRE_LIB = "/usr/lib/secubox/annuaire"
ANNUAIRE_DB = os.environ.get("ANNUAIRE_DB_PATH", "/var/lib/secubox/annuaire/journal.db")
def _read_directory():
import sys # noqa: PLC0415
if ANNUAIRE_LIB not in sys.path:
sys.path.insert(0, ANNUAIRE_LIB)
try:
from annuaire.log import Journal # noqa: PLC0415
from annuaire.verbs import _get_nodes, _get_offers # noqa: PLC0415
except Exception:
return [], {}
if not os.path.exists(ANNUAIRE_DB):
return [], {}
try:
j = Journal(ANNUAIRE_DB)
return _get_offers(j), {n["did"]: n for n in _get_nodes(j)}
except Exception:
return [], {}
@app.get("/mesh-catalog")
async def mesh_catalog():
offers, nodes = _read_directory()
items = []
for o in offers:
prov = o.get("provider")
node = nodes.get(prov, {})
scope = o.get("scope") or {}
items.append({
"name": o.get("name"),
"kind": o.get("kind"),
"endpoint": o.get("endpoint"),
"channel": scope.get("channel"),
"port": scope.get("port"),
"approval_mode": o.get("approval_mode"),
"provider_did": prov,
"provider_node": node.get("boxname") or "?",
"provider_mesh_ip": node.get("mesh_ip"),
})
items.sort(key=lambda x: (x.get("provider_node") or "", x.get("name") or ""))
return {
"catalog": items,
"nodes": [{"boxname": n.get("boxname"), "mesh_ip": n.get("mesh_ip"),
"ddns": n.get("ddns"), "did": n.get("did")} for n in nodes.values()],
"count": len(items), "node_count": len(nodes),
}
@app.get("/module/{name}")
async def module(name: str):
st = compute_state()

View File

@ -1,3 +1,14 @@
secubox-appstore (0.3.0-1~bookworm1) bookworm; urgency=medium
* feat(#768): federated Mesh Catalog (appstore federation phase 2).
GET /mesh-catalog reads this node's converged annuaire directory (read-only)
and joins each ServiceOffer to the publishing node's NodeRecord, so the app
store shows a fleet-wide view: which service is offered/emancipated, by which
node, over which channel, and how to reach it. New "Mesh Catalog" panel in
the UI. Graceful no-op when secubox-annuaire is absent.
-- Gerald Kerma <devel@cybermind.fr> Wed, 01 Jul 2026 11:00:00 +0200
secubox-appstore (0.2.2-1~bookworm1) bookworm; urgency=medium
* fix(menu): install the navbar entry to /usr/share/secubox/menu.d (the

View File

@ -68,6 +68,18 @@
<span class="as-stat" id="stat">loading…</span>
</div>
<div class="as-grid" id="grid"></div>
<section id="mesh-panel" style="margin-top:24px">
<h2 style="font-family:'Cinzel',serif;color:var(--gold-hermetic,#c9a84c);font-size:1rem;margin:0">
🌐 Mesh Catalog <span id="mesh-stat" style="font-size:12px;color:var(--text-muted,#6b6b7a);font-family:'JetBrains Mono',monospace"></span></h2>
<p style="font-size:11px;color:var(--text-muted,#6b6b7a);margin:3px 0 8px;font-family:'JetBrains Mono',monospace">
Services federated across the gondwana mesh (annuaire directory) — which node offers each, over which channel, and how to reach it.</p>
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="text-align:left;color:var(--gold-hermetic,#c9a84c)">
<th style="padding:6px 8px">Service</th><th>Kind</th><th>Node</th><th>Channel</th><th>Reach</th></tr></thead>
<tbody id="mesh-list"><tr><td colspan="5" style="padding:10px;color:var(--text-muted,#6b6b7a)">loading…</td></tr></tbody>
</table>
</section>
</main>
<!-- Config / detail drawer -->
@ -105,6 +117,25 @@
ALL=(data&&Array.isArray(data.modules))?data.modules:[];
document.getElementById('board-badge').textContent='tier '+((cats&&cats.board_tier)||'?');
render();
loadMesh();
}
async function loadMesh(){
const d=await jget('/mesh-catalog');
const tb=document.getElementById('mesh-list'), st=document.getElementById('mesh-stat');
const items=(d&&Array.isArray(d.catalog))?d.catalog:[];
st.textContent=items.length?(items.length+' service(s) · '+(d.node_count||0)+' node(s)'):'no federated services';
if(!items.length){tb.innerHTML='<tr><td colspan="5" style="padding:10px;color:var(--text-muted,#6b6b7a)">No services emancipated across the mesh yet.</td></tr>';return;}
tb.innerHTML=items.map(s=>{
const reach=s.endpoint?('<a href="'+esc(s.endpoint)+'" target="_blank" rel="noopener" style="color:var(--cyber-cyan,#00d4ff)">'+esc(s.endpoint)+'</a>'):'—';
const ip=s.provider_mesh_ip?(' <span style="color:var(--text-muted,#6b6b7a)">('+esc(s.provider_mesh_ip)+')</span>'):'';
return '<tr style="border-top:1px solid var(--line,#2a2a3a)">'
+'<td style="padding:6px 8px">'+esc(s.name)+'</td>'
+'<td><span style="font-size:10px;padding:1px 6px;border:1px solid var(--line,#2a2a3a);border-radius:10px;color:var(--cyber-cyan,#00d4ff)">'+esc(s.kind||'')+'</span></td>'
+'<td style="font-family:\'JetBrains Mono\',monospace">'+esc(s.provider_node||'?')+ip+'</td>'
+'<td>'+esc(s.channel||'—')+'</td>'
+'<td style="font-family:\'JetBrains Mono\',monospace">'+reach+'</td></tr>';
}).join('');
}
function actionsFor(m){