diff --git a/packages/secubox-appstore/api/main.py b/packages/secubox-appstore/api/main.py index 68f74c27..be96dc32 100644 --- a/packages/secubox-appstore/api/main.py +++ b/packages/secubox-appstore/api/main.py @@ -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() diff --git a/packages/secubox-appstore/debian/changelog b/packages/secubox-appstore/debian/changelog index 0cb58033..7a5e6039 100644 --- a/packages/secubox-appstore/debian/changelog +++ b/packages/secubox-appstore/debian/changelog @@ -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 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 diff --git a/packages/secubox-appstore/www/appstore/index.html b/packages/secubox-appstore/www/appstore/index.html index 36782727..18254725 100644 --- a/packages/secubox-appstore/www/appstore/index.html +++ b/packages/secubox-appstore/www/appstore/index.html @@ -68,6 +68,18 @@ loading…
+ +
+

+ 🌐 Mesh Catalog

+

+ Services federated across the gondwana mesh (annuaire directory) — which node offers each, over which channel, and how to reach it.

+ + + + +
ServiceKindNodeChannelReach
loading…
+
@@ -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='No services emancipated across the mesh yet.';return;} + tb.innerHTML=items.map(s=>{ + const reach=s.endpoint?(''+esc(s.endpoint)+''):'—'; + const ip=s.provider_mesh_ip?(' ('+esc(s.provider_mesh_ip)+')'):''; + return '' + +''+esc(s.name)+'' + +''+esc(s.kind||'')+'' + +''+esc(s.provider_node||'?')+ip+'' + +''+esc(s.channel||'—')+'' + +''+reach+''; + }).join(''); } function actionsFor(m){