{task.title}
{task.summary}
{task.owner} · {task.due} · #{task.tag}{task.title}
+{task.summary}
+ {task.owner} / {task.due} / #{task.tag} + {task.completed ? 'Completed' : 'Open'} +diff --git a/.superpowers/sdd/final-fix-report.md b/.superpowers/sdd/final-fix-report.md new file mode 100644 index 0000000..5dcc243 --- /dev/null +++ b/.superpowers/sdd/final-fix-report.md @@ -0,0 +1,27 @@ +# Final Review Fix Report + +## Status + +Completed the final UI review fixes for the Discord-style project workbench. + +## Fixed + +- Cleared the selected inspector item on project and channel changes, with a Project A1 to Project A2 regression test. +- Reworked the mobile breakpoint into a compact horizontal project rail with bounded channel, content, and inspector regions; added a 390px Playwright assertion. +- Prevented inspector grid tracks from stretching compact controls. +- Added structured styles for all rendered channel rows, cards, sessions, metrics, and custom-link controls. +- Replaced the inert task status with a local, accessible checkbox completion control and coverage. +- Validated empty and malformed server values before normalization and added visible login connection status text. +- Disabled unfinished shell commands and converted inert tags and recent sessions to non-interactive list content. +- Rendered sorted channel icons, strengthened per-channel template coverage, and added copy feedback plus focus styling. + +## Verification + +- `npm test -- --run`: 6 files, 17 tests passed. +- `npm run build`: passed. +- `npx playwright test`: 2 Chromium tests passed. +- `git diff --check`: passed. + +## Concern + +- Vite continues to report the pre-existing notice that no Svelte config is present; it did not affect tests or the production build. diff --git a/apps/web/e2e/project-workbench.spec.ts b/apps/web/e2e/project-workbench.spec.ts index b1fee8b..08b02ee 100644 --- a/apps/web/e2e/project-workbench.spec.ts +++ b/apps/web/e2e/project-workbench.spec.ts @@ -24,3 +24,26 @@ test('login and project channel workbench flow', async ({ page }) => { await page.getByRole('button', { name: 'Ops Dashboard' }).click(); await expect(page.getByText('Open external channel')).toBeVisible(); }); + +test('mobile workbench keeps project navigation and inspector compact', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + + await page.getByLabel('Server IP or domain').fill('localhost:8080'); + await page.getByLabel('Email or username').fill('david@example.com'); + await page.getByLabel('Password').fill('secret'); + await page.getByRole('button', { name: 'Log in' }).click(); + + const rail = page.getByLabel('Project list'); + const sidebar = page.getByLabel('Project channels'); + const stage = page.getByLabel('Channel content'); + const inspector = page.getByLabel('Object inspector'); + + await expect(rail).toHaveCSS('grid-auto-flow', 'column'); + await expect(rail).toHaveCSS('max-height', '76px'); + await expect(inspector).toHaveCSS('max-height', '288px'); + + const [sidebarBox, stageBox] = await Promise.all([sidebar.boundingBox(), stage.boundingBox()]); + expect(sidebarBox?.y).toBeLessThan(stageBox?.y ?? 0); + expect((stageBox?.y ?? 0) - (sidebarBox?.y ?? 0)).toBeLessThanOrEqual(248); +}); diff --git a/apps/web/src/features/auth/ServerLogin.svelte b/apps/web/src/features/auth/ServerLogin.svelte index 8e16cb9..be882aa 100644 --- a/apps/web/src/features/auth/ServerLogin.svelte +++ b/apps/web/src/features/auth/ServerLogin.svelte @@ -6,25 +6,44 @@ let account = ''; let password = ''; let error = ''; + let connectionStatus = 'Not connected. Enter a server address to continue.'; - function normalizeServer(value: string) { + function normalizeServer(value: string): { apiBase?: string; error?: string } { const trimmed = value.trim(); - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - return trimmed; + if (!trimmed) { + return { error: 'Enter a server address.' }; + } + + const candidate = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; + + try { + const parsed = new URL(candidate); + if (!parsed.hostname || !['http:', 'https:'].includes(parsed.protocol)) { + return { error: 'Enter a valid HTTP or HTTPS server address.' }; + } + return { apiBase: candidate }; + } catch { + return { error: 'Enter a valid HTTP or HTTPS server address.' }; } - return `http://${trimmed}`; } function submitLogin() { error = ''; + const normalized = normalizeServer(server); + if (normalized.error) { + error = normalized.error; + connectionStatus = 'Check the server address before signing in.'; + return; + } + if (!account.trim() || !password.trim()) { error = 'Enter an account and password.'; return; } - const normalized = normalizeServer(server); - server = normalized; - onLogin({ apiBase: normalized, account: account.trim() }); + server = normalized.apiBase ?? server; + connectionStatus = 'Connection settings are ready.'; + onLogin({ apiBase: server, account: account.trim() }); } @@ -56,6 +75,8 @@
{error}
{/if} +{connectionStatus}
+The server address is remembered on this device.
diff --git a/apps/web/src/features/auth/ServerLogin.test.ts b/apps/web/src/features/auth/ServerLogin.test.ts index 7298b4b..96887df 100644 --- a/apps/web/src/features/auth/ServerLogin.test.ts +++ b/apps/web/src/features/auth/ServerLogin.test.ts @@ -11,6 +11,7 @@ describe('ServerLogin', () => { expect(screen.getByLabelText('Email or username')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent('Not connected. Enter a server address to continue.'); }); it('normalizes server address and emits login details', async () => { @@ -46,4 +47,20 @@ describe('ServerLogin', () => { expect(screen.getByText('Enter an account and password.')).toBeInTheDocument(); }); + + it.each([ + ['', 'Enter a server address.'], + ['http:// bad-server', 'Enter a valid HTTP or HTTPS server address.'], + ])('rejects an invalid server value: %s', async (server, error) => { + const onLogin = vi.fn(); + render(ServerLogin, { props: { apiBase: 'http://localhost:8080', onLogin } }); + + await fireEvent.input(screen.getByLabelText('Server IP or domain'), { target: { value: server } }); + 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(screen.getByText(error)).toBeInTheDocument(); + expect(onLogin).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/src/features/workbench/ChannelContent.test.ts b/apps/web/src/features/workbench/ChannelContent.test.ts index 41c4368..63b0dd7 100644 --- a/apps/web/src/features/workbench/ChannelContent.test.ts +++ b/apps/web/src/features/workbench/ChannelContent.test.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom/vitest'; -import { fireEvent, render, screen } from '@testing-library/svelte'; +import { fireEvent, render, screen, within } from '@testing-library/svelte'; import { describe, expect, it } from 'vitest'; import ChannelContent from './ChannelContent.svelte'; import { getProjectWorkspace } from './mockData'; @@ -7,15 +7,25 @@ import { getProjectWorkspace } from './mockData'; describe('ChannelContent', () => { const workspace = getProjectWorkspace(1); - it('renders different templates for system and custom channels', () => { - for (const type of ['overview', 'inbox', 'tasks', 'ai_sessions', 'notes_sources', 'cron', 'custom_link'] as const) { + it('renders a distinctive heading or control for every channel type', () => { + const expectedContent = { + overview: { role: 'heading', name: 'Overview' }, + inbox: { role: 'heading', name: 'Inbox' }, + tasks: { role: 'checkbox', name: 'Mark Confirm homepage information architecture complete' }, + ai_sessions: { role: 'heading', name: 'AI Sessions' }, + notes_sources: { role: 'heading', name: 'Notes & Sources' }, + cron: { role: 'heading', name: 'Cron Plans' }, + custom_link: { role: 'link', name: 'Open external channel' }, + } as const; + + for (const type of Object.keys(expectedContent) as ArrayExternal resource
{channel.url}
- Open external channel -{copyStatus}
+ {/if}Work Plan
{task.summary}
{task.owner} · {task.due} · #{task.tag}{task.summary}
+ {task.owner} / {task.due} / #{task.tag} + {task.completed ? 'Completed' : 'Open'} +