# Review package Task 1

## Commits
5b95668 feat: add full workbench login page

## Stat
 apps/web/src/app/App.svelte                    | 79 ++++----------------------
 apps/web/src/features/auth/ServerLogin.svelte  | 63 +++++++++++++++-----
 apps/web/src/features/auth/ServerLogin.test.ts | 49 ++++++++++++++++
 3 files changed, 110 insertions(+), 81 deletions(-)

## Diff
diff --git a/apps/web/src/app/App.svelte b/apps/web/src/app/App.svelte
index a4c6389..a5d8dce 100644
--- a/apps/web/src/app/App.svelte
+++ b/apps/web/src/app/App.svelte
@@ -1,76 +1,21 @@
 <script lang="ts">
-  import { onMount } from 'svelte';
-  import ProjectDashboard from '../features/projects/ProjectDashboard.svelte';
   import ServerLogin from '../features/auth/ServerLogin.svelte';
-  import SuggestionList from '../features/inbox/SuggestionList.svelte';
-  import type { Suggestion } from '../features/inbox/types';
-  import { createApi, type ProjectDashboardSummary } from '../lib/api';
-
-  const emptyDashboard: ProjectDashboardSummary = {
-    project_id: 1,
-    pending_inbox_count: 0,
-    open_task_count: 0,
-    recent_note_count: 0,
-    recent_session_count: 0,
-  };
 
   let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
-  let dashboard = emptyDashboard;
-  let connectionStatus = '未连接';
-  let selectedSuggestionCount = 0;
-
-  const sampleSuggestions: Suggestion[] = [
-    {
-      kind: 'task',
-      title: '跟进报价',
-      body: '从项目 inbox 确认后创建任务，并保留来源记录。',
-    },
-    {
-      kind: 'note',
-      title: '客户背景',
-      body: '把对话中的背景信息沉淀成项目笔记。',
-    },
-  ];
+  let currentUser: { account: string } | null = null;
 
-  onMount(() => {
-    void loadDashboard();
-  });
-
-  function handleServerChange(event: CustomEvent<{ apiBase: string }>) {
-    apiBase = event.detail.apiBase;
+  function handleLogin(detail: { apiBase: string; account: string }) {
+    apiBase = detail.apiBase;
     localStorage.setItem('apiBase', apiBase);
-    void loadDashboard();
-  }
-
-  async function loadDashboard() {
-    connectionStatus = '连接中';
-    try {
-      dashboard = await createApi(apiBase).getProjectDashboard(1);
-      connectionStatus = '已连接';
-    } catch {
-      dashboard = emptyDashboard;
-      connectionStatus = '无法连接服务器';
-    }
-  }
-
-  function handleConfirm(selected: Suggestion[]) {
-    selectedSuggestionCount = selected.length;
+    currentUser = { account: detail.account };
   }
 </script>
 
-<main class="shell">
-  <aside class="sidebar">
-    <h1>项目工作台</h1>
-    <ServerLogin {apiBase} on:serverChange={handleServerChange} />
-    <p class="connection-status" aria-live="polite">{connectionStatus}</p>
-  </aside>
-
-  <section class="workspace">
-    <ProjectDashboard summary={dashboard} />
-    <section class="inbox-review" aria-label="项目 inbox">
-      <h2>AI 整理建议</h2>
-      <SuggestionList suggestions={sampleSuggestions} onConfirm={handleConfirm} />
-      <p class="selection-status" aria-live="polite">已选择 {selectedSuggestionCount} 项</p>
-    </section>
-  </section>
-</main>
+{#if currentUser}
+  <main class="workbench-placeholder" aria-label="Project workbench">
+    <h1>SenlinAI Workbench</h1>
+    <p>Signed in as {currentUser.account}</p>
+  </main>
+{:else}
+  <ServerLogin {apiBase} onLogin={handleLogin} />
+{/if}
diff --git a/apps/web/src/features/auth/ServerLogin.svelte b/apps/web/src/features/auth/ServerLogin.svelte
index 8e80261..8e16cb9 100644
--- a/apps/web/src/features/auth/ServerLogin.svelte
+++ b/apps/web/src/features/auth/ServerLogin.svelte
@@ -1,28 +1,63 @@
 <script lang="ts">
-  import { createEventDispatcher } from 'svelte';
-
   export let apiBase: string;
+  export let onLogin: (detail: { apiBase: string; account: string }) => void = () => {};
 
-  const dispatch = createEventDispatcher<{ serverChange: { apiBase: string } }>();
   let server = apiBase;
-
-  function saveServer() {
-    const normalized = normalizeServer(server);
-    server = normalized;
-    dispatch('serverChange', { apiBase: normalized });
-  }
+  let account = '';
+  let password = '';
+  let error = '';
 
   function normalizeServer(value: string) {
     const trimmed = value.trim();
     if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
       return trimmed;
     }
     return `http://${trimmed}`;
   }
+
+  function submitLogin() {
+    error = '';
+    if (!account.trim() || !password.trim()) {
+      error = 'Enter an account and password.';
+      return;
+    }
+
+    const normalized = normalizeServer(server);
+    server = normalized;
+    onLogin({ apiBase: normalized, account: account.trim() });
+  }
 </script>
 
-<form aria-label="服务器登录" class="server-login" on:submit|preventDefault={saveServer}>
-  <label for="server-address">服务器 IP 或域名</label>
-  <input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />
-  <button type="submit">保存服务器</button>
-</form>
+<main class="login-page">
+  <section class="login-panel" aria-labelledby="login-title">
+    <div class="login-brand" aria-hidden="true">SA</div>
+    <div class="login-heading">
+      <p>Private workbench</p>
+      <h1 id="login-title">SenlinAI Workbench</h1>
+    </div>
+
+    <form class="server-login" aria-label="Server login" on:submit|preventDefault={submitLogin}>
+      <label for="server-address">Server IP or domain</label>
+      <input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />
+
+      <label for="login-account">Email or username</label>
+      <input id="login-account" name="account" bind:value={account} autocomplete="username" />
+
+      <label for="login-password">Password</label>
+      <input
+        id="login-password"
+        name="password"
+        type="password"
+        bind:value={password}
+        autocomplete="current-password"
+      />
+
+      {#if error}
+        <p class="form-error" aria-live="polite">{error}</p>
+      {/if}
+
+      <button type="submit">Log in</button>
+      <p class="login-note">The server address is remembered on this device.</p>
+    </form>
+  </section>
+</main>
diff --git a/apps/web/src/features/auth/ServerLogin.test.ts b/apps/web/src/features/auth/ServerLogin.test.ts
new file mode 100644
index 0000000..7298b4b
--- /dev/null
+++ b/apps/web/src/features/auth/ServerLogin.test.ts
@@ -0,0 +1,49 @@
+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();
+  });
+});
