diff --git a/docs/PLAN.md b/docs/PLAN.md
index 04fb45b..396d1c2 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -21,3 +21,4 @@ Updated: 2026-04-01
5. Simplified the UI into a calmer minimalist layout with reduced visual noise and denser operational presentation.
6. Moved user creation into a modal flow and tightened the operator UX with quieter navigation and a denser users table.
7. Rebuilt the UI shell from scratch around a stable topbar/tab layout with fixed typography and lower visual noise across window sizes.
+8. Corrected the user-creation flow to select a 3proxy service instead of assigning a per-user port, matching the documented 3proxy model.
diff --git a/docs/PROJECT_INDEX.md b/docs/PROJECT_INDEX.md
index 63d1b22..e04ecb7 100644
--- a/docs/PROJECT_INDEX.md
+++ b/docs/PROJECT_INDEX.md
@@ -21,7 +21,7 @@ Updated: 2026-04-01
- `src/App.tsx`: authenticated panel shell rebuilt around a topbar/tab layout, plus modal user creation flow
- `src/App.test.tsx`: login-gate and modal interaction tests
- `src/app.css`: full panel styling
-- `src/data/mockDashboard.ts`: realistic mock state shaped for future API responses
+- `src/data/mockDashboard.ts`: realistic mock state shaped for future API responses, including service-bound users
- `src/lib/3proxy.ts`: formatting and status helpers
- `src/lib/3proxy.test.ts`: paranoia-oriented tests for core domain rules
- `src/test/setup.ts`: Testing Library matchers
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 7102d2e..d868414 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen } from '@testing-library/react';
+import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import App from './App';
@@ -40,8 +40,13 @@ describe('App login gate', () => {
await user.click(screen.getByRole('button', { name: /users/i }));
await user.click(screen.getByRole('button', { name: /new user/i }));
- expect(screen.getByRole('dialog', { name: /add user/i })).toBeInTheDocument();
+ const dialog = screen.getByRole('dialog', { name: /add user/i });
+
+ expect(dialog).toBeInTheDocument();
expect(screen.getByPlaceholderText(/night-shift-01/i)).toBeInTheDocument();
+ expect(screen.getByLabelText(/service/i)).toBeInTheDocument();
+ expect(screen.queryByLabelText(/port/i)).not.toBeInTheDocument();
+ expect(within(dialog).getByText(/edge\.example\.net:1080/i)).toBeInTheDocument();
await user.keyboard('{Escape}');
diff --git a/src/App.tsx b/src/App.tsx
index 7d53dbd..065d896 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,6 +18,14 @@ const tabs: Array<{ id: TabId; label: string }> = [
{ id: 'system', label: 'System' },
];
+const assignableServices = dashboardSnapshot.system.services.filter(
+ (service) => service.enabled && service.assignable,
+);
+
+const servicesById = new Map(
+ dashboardSnapshot.system.services.map((service) => [service.id, service] as const),
+);
+
function LoginGate({ onUnlock }: { onUnlock: () => void }) {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
@@ -71,6 +79,8 @@ function LoginGate({ onUnlock }: { onUnlock: () => void }) {
}
function AddUserModal({ onClose }: { onClose: () => void }) {
+ const [serviceId, setServiceId] = useState(assignableServices[0]?.id ?? '');
+
useEffect(() => {
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
if (event.key === 'Escape') {
@@ -87,6 +97,8 @@ function AddUserModal({ onClose }: { onClose: () => void }) {
event.stopPropagation();
};
+ const selectedService = servicesById.get(serviceId);
+
return (
void }) {
+
+ Endpoint
+
+ {selectedService
+ ? `${dashboardSnapshot.system.publicHost}:${selectedService.port}`
+ : 'Unavailable'}
+
+
+
+ Protocol
+ {selectedService ? selectedService.protocol : 'Unavailable'}
+