feat: Services UI — nested nav, per-service pages, settings migration

Rename Store → Services: ServicesIndex.jsx shows built-in core services
(Email, Calendar, Files) with Manage links, plus the existing add-on
store below.

New service sub-pages at /services/email|calendar|files serve both
admin and peer roles. Admins see connection info, service status, users
list, and an inline config form (port/data-dir). Peers see connection
info and their personal credentials fetched from peerAPI.

Navigation restructured: a Services parent item expands to show the
three sub-pages via a collapsible sidebar group (ChevronDown toggle).
Both admin and peer navigation include the Services group. Sidebar
extracted NavItem/NavList components to eliminate the duplicate mobile/
desktop rendering.

Settings.jsx drops EmailForm, CalendarForm, FilesForm and their
SERVICE_DEFS entries. Port conflict detection and per-service validation
logic extracted to utils/serviceConfig.js, shared by Settings and the
new service pages. Service form flushers are registered without cleanup
so the Apply banner saves dirty config even when the user navigates away
from a service page before clicking Apply.

Legacy routes /email, /calendar, /files, /store redirect to their new
canonical paths.

GET /api/config now includes installed_services so the nav can derive
which add-ons are installed without a separate store fetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 06:46:17 -04:00
parent b16189d00f
commit 0afdee32da
11 changed files with 1684 additions and 309 deletions
+38 -28
View File
@@ -1,4 +1,4 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useState, useEffect, useCallback } from 'react';
import {
Home,
@@ -30,9 +30,6 @@ import Dashboard from './pages/Dashboard';
import Peers from './pages/Peers';
import NetworkServices from './pages/NetworkServices';
import WireGuard from './pages/WireGuard';
import Email from './pages/Email';
import Calendar from './pages/Calendar';
import Files from './pages/Files';
import Routing from './pages/Routing';
import Logs from './pages/Logs';
import Settings from './pages/Settings';
@@ -43,7 +40,10 @@ import Login from './pages/Login';
import AccountSettings from './pages/AccountSettings';
import PeerDashboard from './pages/PeerDashboard';
import MyServices from './pages/MyServices';
import Store from './pages/Store';
import ServicesIndex from './pages/ServicesIndex';
import EmailPage from './pages/services/EmailPage';
import CalendarPage from './pages/services/CalendarPage';
import FilesPage from './pages/services/FilesPage';
import Connectivity from './pages/Connectivity';
import Setup from './pages/Setup';
import SetupGuard from './components/SetupGuard';
@@ -230,29 +230,33 @@ 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 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: 'Email', href: '/email', icon: Mail },
{ name: 'Calendar', href: '/calendar', icon: CalendarIcon },
{ name: 'Files', href: '/files', icon: FolderOpen },
{ name: 'Routing', href: '/routing', icon: Wifi },
{ name: 'Vault', href: '/vault', icon: Key },
{ name: 'Containers', href: '/containers', icon: Package2 },
{ name: 'Store', href: '/store', icon: Package },
{ name: 'Cell Network', href: '/cell-network', icon: Link2 },
{ name: 'Connectivity', href: '/connectivity', icon: Network },
{ name: 'Logs', href: '/logs', icon: Activity },
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
{ name: 'Account', href: '/account', icon: User },
{ 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: 'Routing', href: '/routing', icon: Wifi },
{ name: 'Vault', href: '/vault', icon: Key },
{ name: 'Containers', href: '/containers', icon: Package2 },
{ name: 'Cell Network', href: '/cell-network', icon: Link2 },
{ name: 'Connectivity', href: '/connectivity', icon: Network },
{ name: 'Logs', href: '/logs', icon: Activity },
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
{ name: 'Account', href: '/account', icon: User },
];
const peerNavigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'My Services', href: '/my-services', icon: FolderOpen },
{ name: 'Account', href: '/account', icon: User },
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'My Services', href: '/my-services', icon: Wifi },
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
{ name: 'Account', href: '/account', icon: User },
];
const { user } = useAuth();
@@ -339,16 +343,22 @@ function AppCore() {
<Route path="/" element={<PrivateRoute><RoleHome isOnline={isOnline} /></PrivateRoute>} />
<Route path="/account" element={<PrivateRoute><AccountSettings /></PrivateRoute>} />
<Route path="/my-services" element={<PrivateRoute requireRole="peer"><MyServices /></PrivateRoute>} />
{/* Service pages — accessible to both admin and peer (role-conditional content inside) */}
<Route path="/services" element={<PrivateRoute requireRole="admin"><ServicesIndex /></PrivateRoute>} />
<Route path="/services/email" element={<PrivateRoute><EmailPage /></PrivateRoute>} />
<Route path="/services/calendar" element={<PrivateRoute><CalendarPage /></PrivateRoute>} />
<Route path="/services/files" element={<PrivateRoute><FilesPage /></PrivateRoute>} />
{/* Legacy URL redirects */}
<Route path="/email" element={<Navigate to="/services/email" replace />} />
<Route path="/calendar" element={<Navigate to="/services/calendar" replace />} />
<Route path="/files" element={<Navigate to="/services/files" replace />} />
<Route path="/store" element={<Navigate to="/services" replace />} />
<Route path="/peers" element={<PrivateRoute requireRole="admin"><Peers /></PrivateRoute>} />
<Route path="/network" element={<PrivateRoute requireRole="admin"><NetworkServices /></PrivateRoute>} />
<Route path="/wireguard" element={<PrivateRoute requireRole="admin"><WireGuard /></PrivateRoute>} />
<Route path="/email" element={<PrivateRoute requireRole="admin"><Email /></PrivateRoute>} />
<Route path="/calendar" element={<PrivateRoute requireRole="admin"><Calendar /></PrivateRoute>} />
<Route path="/files" element={<PrivateRoute requireRole="admin"><Files /></PrivateRoute>} />
<Route path="/routing" element={<PrivateRoute requireRole="admin"><Routing /></PrivateRoute>} />
<Route path="/vault" element={<PrivateRoute requireRole="admin"><Vault /></PrivateRoute>} />
<Route path="/containers" element={<PrivateRoute requireRole="admin"><ContainerDashboard /></PrivateRoute>} />
<Route path="/store" element={<PrivateRoute requireRole="admin"><Store /></PrivateRoute>} />
<Route path="/cell-network" element={<PrivateRoute requireRole="admin"><CellNetwork /></PrivateRoute>} />
<Route path="/connectivity" element={<PrivateRoute requireRole="admin"><Connectivity /></PrivateRoute>} />
<Route path="/logs" element={<PrivateRoute requireRole="admin"><Logs /></PrivateRoute>} />