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:
@@ -76,13 +76,44 @@ class TestAPIEndpoints(unittest.TestCase):
|
||||
"""Test get config endpoint"""
|
||||
response = self.client.get('/api/config')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('cell_name', data)
|
||||
self.assertIn('domain', data)
|
||||
self.assertIn('ip_range', data)
|
||||
self.assertIn('wireguard_port', data)
|
||||
self.assertIn('installed_services', data)
|
||||
|
||||
def test_get_config_installed_services_is_dict(self):
|
||||
"""installed_services must be a dict, never a list or primitive"""
|
||||
response = self.client.get('/api/config')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIsInstance(data['installed_services'], dict)
|
||||
|
||||
def test_get_config_installed_services_empty_when_none_installed(self):
|
||||
"""installed_services defaults to empty dict when no services are installed"""
|
||||
response = self.client.get('/api/config')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
# Fresh test environment has no installed services
|
||||
self.assertEqual(data['installed_services'], {})
|
||||
|
||||
def test_get_config_installed_services_reflects_stored_value(self):
|
||||
"""installed_services in GET /api/config reflects what config_manager returns"""
|
||||
from app import config_manager
|
||||
config_manager.configs.setdefault('_identity', {})['installed_services'] = {
|
||||
'mailserver': {'status': 'running', 'installed_at': '2026-01-01T00:00:00'}
|
||||
}
|
||||
try:
|
||||
response = self.client.get('/api/config')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('mailserver', data['installed_services'])
|
||||
self.assertEqual(data['installed_services']['mailserver']['status'], 'running')
|
||||
finally:
|
||||
config_manager.configs.get('_identity', {}).pop('installed_services', None)
|
||||
|
||||
def test_update_config_endpoint(self):
|
||||
"""Test update config endpoint"""
|
||||
update_data = {'cell_name': 'newcell'}
|
||||
|
||||
Reference in New Issue
Block a user