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
+28 -8
View File
@@ -20,7 +20,7 @@ import {
AlertTriangle,
User,
} from 'lucide-react';
import { healthAPI, cellAPI } from './services/api';
import { healthAPI, cellAPI, servicesAPI } from './services/api';
import { ConfigProvider } from './contexts/ConfigContext';
import { DraftConfigProvider, useDraftConfig } from './contexts/DraftConfigContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
@@ -48,6 +48,12 @@ import Connectivity from './pages/Connectivity';
import Setup from './pages/Setup';
import SetupGuard from './components/SetupGuard';
const SERVICE_META = {
email: { name: 'Email', href: '/services/email', icon: Mail },
calendar: { name: 'Calendar', href: '/services/calendar', icon: CalendarIcon },
files: { name: 'File Storage', href: '/services/files', icon: FolderOpen },
};
function PendingRestartBanner({ pending, onApply, onCancel }) {
const [confirming, setConfirming] = useState(false);
const [applying, setApplying] = useState(false);
@@ -230,18 +236,32 @@ function AppCore() {
window.dispatchEvent(new CustomEvent('pic-config-discarded'));
}, []);
const coreServiceChildren = [
{ name: 'Email', href: '/services/email', icon: Mail },
{ name: 'Calendar', href: '/services/calendar', icon: CalendarIcon },
{ name: 'Files', href: '/services/files', icon: FolderOpen },
];
const [activeServiceChildren, setActiveServiceChildren] = useState([]);
const fetchActiveServices = useCallback(async () => {
try {
const resp = await servicesAPI.listActive();
const children = (resp.data || [])
.filter(svc => SERVICE_META[svc.id])
.map(svc => SERVICE_META[svc.id]);
setActiveServiceChildren(children);
} catch {
// silent — empty nav children is safe
}
}, []);
useEffect(() => {
fetchActiveServices();
window.addEventListener('pic-services-changed', fetchActiveServices);
return () => window.removeEventListener('pic-services-changed', fetchActiveServices);
}, [fetchActiveServices]);
const adminNavigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'Peers', href: '/peers', icon: Users },
{ name: 'Network Services', href: '/network', icon: Network },
{ name: 'WireGuard', href: '/wireguard', icon: Shield },
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
{ name: 'Services', href: '/services', icon: Package, children: activeServiceChildren },
{ name: 'Routing', href: '/routing', icon: Wifi },
{ name: 'Vault', href: '/vault', icon: Key },
{ name: 'Containers', href: '/containers', icon: Package2 },
@@ -255,7 +275,7 @@ function AppCore() {
const peerNavigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'My Services', href: '/my-services', icon: Wifi },
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
{ name: 'Services', href: '/services', icon: Package, children: activeServiceChildren },
{ name: 'Account', href: '/account', icon: User },
];