feat: Phase 4 — dynamic nav + service visibility based on installed services
Unit Tests / test (push) Successful in 11m24s

Email, calendar, and files no longer appear in the nav or as usable pages
unless they are installed. The nav refreshes whenever a service is installed
or removed via the new pic-services-changed CustomEvent.

Changes:
- routes/services.py: add GET /api/services/active endpoint
- api.js: add servicesAPI.listActive()
- App.jsx: replace hardcoded coreServiceChildren with dynamic state fetched
  from /api/services/active; SERVICE_META maps ids to nav entry shapes
- ServiceNotInstalledBanner.jsx: new component — admin gets catalog link,
  peer gets "contact admin" message
- EmailPage/CalendarPage/FilesPage: show banner when service not installed
- ServicesIndex.jsx: remove CoreServiceCard + CORE_SERVICES "Built-in"
  section; rename Remove → Uninstall; dispatch pic-services-changed on
  install/uninstall success
- MyServices.jsx: conditionally render service cards based on active list;
  placeholder card when absent; page-level notice when nothing is installed
- tests/test_services_active_endpoint.py: 4 new endpoint tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 12:15:02 -04:00
parent 87c321c1c9
commit a10fe11136
10 changed files with 308 additions and 109 deletions
+20
View File
@@ -20,6 +20,26 @@ def get_services_catalog():
return jsonify({'error': str(e)}), 500
@bp.route('/api/services/active', methods=['GET'])
def get_active_services():
"""Return minimal info for all installed services. Used by webui to build nav."""
try:
from app import service_registry
active = service_registry.list_active()
return jsonify([
{
'id': svc['id'],
'name': svc.get('name', svc['id']),
'subdomain': svc.get('subdomain'),
'capabilities': svc.get('capabilities', {}),
}
for svc in active
])
except Exception as e:
logger.error('get_active_services: %s', e)
return jsonify({'error': str(e)}), 500
@bp.route('/api/services/catalog/<service_id>', methods=['GET'])
def get_service_catalog_entry(service_id: str):
"""Return a single service manifest+config, or 404 if unknown."""