diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfa02b..220649f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,21 @@ follows [Semantic Versioning](https://semver.org/). _Feature module implementation (iam, billing, workspace, discovery) in progress._ +### Fixed (Session history stats and duration — `bugfix/discovery-history-stats-contract`) + +- **The session history now shows its stats columns and the duration of live sessions.** The session + model expected `storiesGeneratedCount`, `storiesAcceptedCount`, `pendingSuggestionsCount` and + `questionsCount`, but the API sends `storiesGenerated`, `storiesAccepted`, `suggestionsPending` and + `questionsAsked`. As a result: + - the Historias / Aceptadas / Pendientes / Preguntas columns never appeared; + - the session separator in Captura never showed its story count. + + The duration cell only read `audioDurationMs`, which live sessions never set, so every live session + showed "—". +- The model now uses the API names and adds `durationSeconds`. The duration cell prefers it and still + falls back to `audioDurationMs` for older deployments. The logic lives in the new pure helpers + `sessionDuration` and `hasSessionStats` (`history.helpers.ts`), which have unit tests. + ### Added - **MVP feature flags** (`feature/mvp-feature-flags`): a typed `features` map in both environment diff --git a/src/app/features/discovery/data/discovery.models.ts b/src/app/features/discovery/data/discovery.models.ts index 34b372e..a7d5d4b 100644 --- a/src/app/features/discovery/data/discovery.models.ts +++ b/src/app/features/discovery/data/discovery.models.ts @@ -15,12 +15,21 @@ export interface DiscoverySessionResponse { processingError: string | null; createdAt: string; updatedAt: string; - // Per-session stats being added by a parallel backend branch — absent on older - // deployments, so every consumer must degrade gracefully when undefined. - storiesGeneratedCount?: number | null; - storiesAcceptedCount?: number | null; - pendingSuggestionsCount?: number | null; - questionsCount?: number | null; + /** + * Recording length in seconds: the audio length of an uploaded recording, otherwise start to stop of + * a live session; null while a live session is still running. + */ + durationSeconds?: number | null; + // Per-session stats, only on the get/list endpoints (null on lifecycle responses such as start/stop), + // so every consumer must degrade gracefully when null or undefined. + /** Backlog stories whose source is this session. */ + storiesGenerated?: number | null; + /** Story suggestions of this session the analyst accepted (questions excluded). */ + storiesAccepted?: number | null; + /** Suggestions of this session still pending review. */ + suggestionsPending?: number | null; + /** Clarifying questions the AI raised in this session. */ + questionsAsked?: number | null; } /** Raw transcript of a session (GET /sessions/{id}/transcript; large text kept off the session resource). */ diff --git a/src/app/features/discovery/pages/discovery-chat/discovery-chat.ts b/src/app/features/discovery/pages/discovery-chat/discovery-chat.ts index fb7f72d..9de2784 100644 --- a/src/app/features/discovery/pages/discovery-chat/discovery-chat.ts +++ b/src/app/features/discovery/pages/discovery-chat/discovery-chat.ts @@ -330,13 +330,13 @@ import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui'; {{ sessionAt | date: 'MMM d · HH:mm' }} } @if ( - block.session.storiesGeneratedCount !== null && - block.session.storiesGeneratedCount !== undefined + block.session.storiesGenerated !== null && + block.session.storiesGenerated !== undefined ) { · {{ 'discovery.sessionStories' - | transloco: { count: block.session.storiesGeneratedCount } + | transloco: { count: block.session.storiesGenerated } }} } diff --git a/src/app/features/discovery/pages/history/history.helpers.spec.ts b/src/app/features/discovery/pages/history/history.helpers.spec.ts new file mode 100644 index 0000000..e0bc681 --- /dev/null +++ b/src/app/features/discovery/pages/history/history.helpers.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { hasSessionStats, sessionDuration } from './history.helpers'; + +describe('sessionDuration', () => { + it('formats the API durationSeconds of a live session', () => { + expect(sessionDuration({ durationSeconds: 117, audioDurationMs: 0 })).toBe('1:57'); + }); + + it('prefers durationSeconds over audioDurationMs', () => { + expect(sessionDuration({ durationSeconds: 138, audioDurationMs: 138_175 })).toBe('2:18'); + }); + + it('falls back to the uploaded audio length when durationSeconds is absent', () => { + expect(sessionDuration({ audioDurationMs: 95_412 })).toBe('1:35'); + }); + + it('shows a dash for a running live session', () => { + expect(sessionDuration({ durationSeconds: null, audioDurationMs: 0 })).toBe('—'); + }); +}); + +describe('hasSessionStats', () => { + it('is true when a session carries storiesGenerated, even zero', () => { + expect(hasSessionStats([{ storiesGenerated: null }, { storiesGenerated: 0 }])).toBe(true); + }); + + it('is false when no session carries stats', () => { + expect(hasSessionStats([{}, { storiesGenerated: null }])).toBe(false); + }); +}); diff --git a/src/app/features/discovery/pages/history/history.helpers.ts b/src/app/features/discovery/pages/history/history.helpers.ts new file mode 100644 index 0000000..c5d2121 --- /dev/null +++ b/src/app/features/discovery/pages/history/history.helpers.ts @@ -0,0 +1,22 @@ +import { formatElapsed } from '../../components/session-bar/session-bar'; +import { DiscoverySessionResponse } from '../../data/discovery.models'; + +/** + * Duration cell of the history table: the API's `durationSeconds` (audio length of an upload, start to + * stop of a live session), else the uploaded audio length for older deployments, else a dash. + */ +export function sessionDuration( + session: Pick, +): string { + if (session.durationSeconds !== null && session.durationSeconds !== undefined) { + return formatElapsed(session.durationSeconds * 1000); + } + return session.audioDurationMs > 0 ? formatElapsed(session.audioDurationMs) : '—'; +} + +/** True when at least one session carries the per-session stats, so the stats columns are shown. */ +export function hasSessionStats( + sessions: readonly Pick[], +): boolean { + return sessions.some((s) => s.storiesGenerated !== null && s.storiesGenerated !== undefined); +} diff --git a/src/app/features/discovery/pages/history/history.ts b/src/app/features/discovery/pages/history/history.ts index cca8e67..becd7ae 100644 --- a/src/app/features/discovery/pages/history/history.ts +++ b/src/app/features/discovery/pages/history/history.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnInit, + computed, + inject, + input, + signal, +} from '@angular/core'; import { DatePipe } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { TranslocoPipe } from '@jsverse/transloco'; @@ -6,7 +14,7 @@ import { provideIcons } from '@ng-icons/core'; import { lucideArrowLeft } from '@ng-icons/lucide'; import { DiscoveryApiService } from '../../data/discovery-api.service'; import { DiscoverySessionResponse } from '../../data/discovery.models'; -import { formatElapsed } from '../../components/session-bar/session-bar'; +import { hasSessionStats, sessionDuration } from './history.helpers'; import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui'; /** @@ -71,7 +79,9 @@ import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui'; {{ 'discovery.history.colStories' | transloco }} {{ 'discovery.history.colAccepted' | transloco }} {{ 'discovery.history.colPending' | transloco }} - {{ 'discovery.history.colQuestions' | transloco }} + + {{ 'discovery.history.colQuestions' | transloco }} + } @@ -98,15 +108,15 @@ import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui'; @if (showStats()) { - {{ session.storiesGeneratedCount ?? '—' }} + {{ session.storiesGenerated ?? '—' }} - {{ session.storiesAcceptedCount ?? '—' }} + {{ session.storiesAccepted ?? '—' }} - {{ session.pendingSuggestionsCount ?? '—' }} + {{ session.suggestionsPending ?? '—' }} - {{ session.questionsCount ?? '—' }} + {{ session.questionsAsked ?? '—' }} } } @@ -151,9 +161,7 @@ export class DiscoveryHistory implements OnInit { private nextPage = 0; /** Show stats columns only when at least one session actually carries them. */ - protected readonly showStats = computed(() => - this.sessions().some((s) => s.storiesGeneratedCount !== null && s.storiesGeneratedCount !== undefined), - ); + protected readonly showStats = computed(() => hasSessionStats(this.sessions())); ngOnInit(): void { this.load(0); @@ -191,7 +199,7 @@ export class DiscoveryHistory implements OnInit { } protected duration(session: DiscoverySessionResponse): string { - return session.audioDurationMs > 0 ? formatElapsed(session.audioDurationMs) : '—'; + return sessionDuration(session); } protected statusClass(status: string): string {