## Task 1: Login Page And App-Level Session
**Files:**
- Modify: `apps/web/src/features/auth/ServerLogin.svelte`
- Modify: `apps/web/src/app/App.svelte`
- Test: `apps/web/src/features/auth/ServerLogin.test.ts`
**Interfaces:**
- Produces: `ServerLogin` Svelte component event `login: { apiBase: string; account: string }`
- Produces: app state fields `apiBase: string`, `currentUser: { account: string } | null`
- Consumes: `localStorage['apiBase']`
- [ ] **Step 1: Write failing login component tests**
Create `apps/web/src/features/auth/ServerLogin.test.ts`:
```ts
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import ServerLogin from './ServerLogin.svelte';
describe('ServerLogin', () => {
it('renders server, account, and password fields', () => {
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument();
expect(screen.getByLabelText('Email or username')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
});
it('normalizes server address and emits login details', async () => {
const onLogin = vi.fn();
render(ServerLogin, {
props: {
apiBase: 'localhost:8080',
onLogin,
},
});
await fireEvent.input(screen.getByLabelText('Server IP or domain'), {
target: { value: '10.0.0.12:8080' },
});
await fireEvent.input(screen.getByLabelText('Email or username'), {
target: { value: 'david@example.com' },
});
await fireEvent.input(screen.getByLabelText('Password'), {
target: { value: 'secret' },
});
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
expect(onLogin).toHaveBeenCalledWith({
apiBase: 'http://10.0.0.12:8080',
account: 'david@example.com',
});
});
it('shows an error when account or password is missing', async () => {
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
expect(screen.getByText('Enter an account and password.')).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run the new test to verify it fails**
Run:
```powershell
Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/auth/ServerLogin.test.ts
```
Expected: FAIL because `ServerLogin` does not render account/password fields and does not expose `onLogin`.
- [ ] **Step 3: Replace `ServerLogin.svelte` with the complete login page**
Implement `apps/web/src/features/auth/ServerLogin.svelte`:
```svelte
SA
Private workbench
SenlinAI Workbench
```
- [ ] **Step 4: Update `App.svelte` to switch from login to workbench**
Temporarily render a simple logged-in placeholder; the full workbench arrives in Task 3.
```svelte
{#if currentUser}