fix: stabilize the live recommendation path
Some checks are pending
ci / backend (push) Waiting to run
ci / frontend (push) Waiting to run

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Visser 2026-08-12 10:47:30 +02:00
parent bfea9efb5e
commit dbc5c2d93f
12 changed files with 113 additions and 31 deletions

View file

@ -48,7 +48,7 @@ const modeLabel = computed(() => {
<span class="mode" :class="{ failed: healthFailed }">
{{ modeLabel }}
</span>
<a v-if="demoUrl" class="demo-link" :href="demoUrl">
<a v-if="demoUrl" class="button demo-link" :href="demoUrl">
{{ messages.appHeaderDemoInstance }}
</a>
<AuthStatus :auth="auth" @logout="$emit('logout')" />
@ -146,21 +146,19 @@ const modeLabel = computed(() => {
}
.demo-link {
color: var(--c-text-chip);
text-transform: uppercase;
text-decoration: none;
border-bottom: 1px solid var(--c-line-strong);
}
.demo-link:hover {
color: var(--c-text);
border-color: var(--c-accent);
}
.button {
display: inline-flex;
flex: none;
align-items: center;
min-height: 31px;
padding: 6px 11px;
color: var(--c-text-muted);
font-family: var(--f-mono);
font-size: var(--t-micro);
cursor: pointer;
background: transparent;
border: 1px solid var(--c-line);
@ -169,6 +167,7 @@ const modeLabel = computed(() => {
.button:hover {
color: var(--c-text);
text-decoration: none;
border-color: var(--c-focus-border);
}

View file

@ -36,6 +36,7 @@ const logoutLabel = computed(() => {
>{{ auth.user.display_name }}
</span>
<button
v-if="auth.user.can_logout"
class="button"
type="button"
:disabled="auth.status === 'logging_out'"

View file

@ -9,10 +9,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}
function parseCurrentUser(value: unknown): CurrentUser {
if (!isRecord(value) || typeof value.display_name !== 'string') {
if (
!isRecord(value) ||
typeof value.display_name !== 'string' ||
typeof value.can_logout !== 'boolean'
) {
throw new Error(messages.useApiInvalidCurrentUser)
}
return { display_name: value.display_name }
return { display_name: value.display_name, can_logout: value.can_logout }
}
function parsePlaylistResponse(value: unknown): { url: string | null } {

View file

@ -76,4 +76,5 @@ export interface PlaylistCreateResponse {
export interface CurrentUser {
display_name: string
can_logout: boolean
}

View file

@ -85,6 +85,48 @@ describe('result presentation', () => {
expect(onReset).toHaveBeenCalledOnce()
})
it('renders demo as a header button and hides logout for a seeded user', () => {
const demoUrl = 'https://demo.example.com'
const root = mountComponent(AppHeader, {
auth: {
status: 'authenticated',
user: { display_name: 'Seed Listener', can_logout: false },
message: null,
},
mode: 'live',
healthFailed: false,
demoUrl,
})
const demoLink = root.querySelector<HTMLAnchorElement>('a.demo-link')
expect(demoLink?.classList.contains('button')).toBe(true)
expect(demoLink?.href).toBe(`${demoUrl}/`)
expect(demoLink?.textContent).toContain('demo')
expect(root.textContent).toContain('Seed Listener')
expect(root.textContent).not.toContain('Log out')
})
it('keeps logout available for an interactive local user', () => {
const onLogout = vi.fn()
const root = mountComponent(AppHeader, {
auth: {
status: 'authenticated',
user: { display_name: 'Local Listener', can_logout: true },
message: null,
},
mode: 'live',
healthFailed: false,
demoUrl: null,
onLogout,
})
const logout = [...root.querySelectorAll<HTMLButtonElement>('button')].find((button) =>
button.textContent?.includes('Log out'),
)
logout?.click()
expect(onLogout).toHaveBeenCalledOnce()
})
it('keeps completion context outside the focusable track region', () => {
const root = mountComponent(AssistantMessage, { turn: doneTurn(), canSave: true })
const region = root.querySelector<HTMLElement>(