Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 15 additions & 6 deletions src/app/features/discovery/data/discovery.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}}
}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
22 changes: 22 additions & 0 deletions src/app/features/discovery/pages/history/history.helpers.ts
Original file line number Diff line number Diff line change
@@ -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<DiscoverySessionResponse, 'durationSeconds' | 'audioDurationMs'>,
): 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<DiscoverySessionResponse, 'storiesGenerated'>[],
): boolean {
return sessions.some((s) => s.storiesGenerated !== null && s.storiesGenerated !== undefined);
}
30 changes: 19 additions & 11 deletions src/app/features/discovery/pages/history/history.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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';
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';

/**
Expand Down Expand Up @@ -71,7 +79,9 @@ import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui';
<th class="px-4 py-2.5">{{ 'discovery.history.colStories' | transloco }}</th>
<th class="px-4 py-2.5">{{ 'discovery.history.colAccepted' | transloco }}</th>
<th class="px-4 py-2.5">{{ 'discovery.history.colPending' | transloco }}</th>
<th class="px-4 py-2.5">{{ 'discovery.history.colQuestions' | transloco }}</th>
<th class="px-4 py-2.5">
{{ 'discovery.history.colQuestions' | transloco }}
</th>
}
</tr>
</thead>
Expand All @@ -98,15 +108,15 @@ import { HlmButton, HlmIcon, HlmSpinner } from '../../../../shared/ui';
</td>
@if (showStats()) {
<td class="px-4 py-3 tabular-nums">
{{ session.storiesGeneratedCount ?? '—' }}
{{ session.storiesGenerated ?? '—' }}
</td>
<td class="px-4 py-3 tabular-nums">
{{ session.storiesAcceptedCount ?? '—' }}
{{ session.storiesAccepted ?? '—' }}
</td>
<td class="px-4 py-3 tabular-nums">
{{ session.pendingSuggestionsCount ?? '—' }}
{{ session.suggestionsPending ?? '—' }}
</td>
<td class="px-4 py-3 tabular-nums">{{ session.questionsCount ?? '—' }}</td>
<td class="px-4 py-3 tabular-nums">{{ session.questionsAsked ?? '—' }}</td>
}
</tr>
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
Loading