From 62d330bdb69db2f58b17066123b5f7a221674492 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Fri, 24 Jul 2026 18:24:13 +0400 Subject: [PATCH] feat(ui): Productivity pilot reusing the analytics dashboard (U5 B2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third B2 slice — the first to reuse the generic AnalyticsDashboard from #439 with no new libs/ui component: - Electron: pure productivity-ui mappers (formatHours; buildProductivity Kpis/Charts/Sections from ProductivitySummary + ProductivityTrend Point, reusing analytics-ui's formatters/shortDate/sampleLabels/ successRateTone; converts the backend 0-1 rates to whole percents; 7 tests) + ProductivityPilotView over getProductivitySummary + getProductivityTrends. Mounted as "Productivity (new UI)". - i18n: analytics:productivityPilot.* keys (en+fr; the productivity screens share the analytics namespace), navigation.productivityNext. Scope: the productivity mockup's categorical bar panels ("when you ship", "where the time went") have no backing data in the report, so — like the Patterns pilot — this maps only the real data: KPI summary figures + the daily trend series as line charts + outcome/effort cards. No BarChart primitive is invented for panels the backend can't fill. Co-Authored-By: Claude Fable 5 --- apps/frontend/src/renderer/App.tsx | 4 + .../__tests__/productivity-ui.test.ts | 130 ++++++++++++++ .../components/ProductivityPilotView.tsx | 159 ++++++++++++++++ .../src/renderer/components/Sidebar.tsx | 3 +- .../src/renderer/lib/productivity-ui.ts | 170 ++++++++++++++++++ .../src/shared/i18n/locales/en/analytics.json | 35 ++++ .../shared/i18n/locales/en/navigation.json | 3 +- .../src/shared/i18n/locales/fr/analytics.json | 35 ++++ .../shared/i18n/locales/fr/navigation.json | 3 +- 9 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 apps/frontend/src/renderer/__tests__/productivity-ui.test.ts create mode 100644 apps/frontend/src/renderer/components/ProductivityPilotView.tsx create mode 100644 apps/frontend/src/renderer/lib/productivity-ui.ts diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index 3017209f2..7ef070661 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -75,6 +75,7 @@ import { GitHubPRsPilotView } from './components/GitHubPRsPilotView'; import { GitHubIssuesPilotView } from './components/GitHubIssuesPilotView'; import { PatternsPilotView } from './components/PatternsPilotView'; import { AnalyticsPilotView } from './components/AnalyticsPilotView'; +import { ProductivityPilotView } from './components/ProductivityPilotView'; import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store'; import { useClaudeProfileStore } from './stores/claude-profile-store'; import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store'; @@ -1143,6 +1144,9 @@ export function App() { {activeView === 'productivity' && (activeProjectId || selectedProjectId) && ( )} + {activeView === 'productivity-next' && (activeProjectId || selectedProjectId) && ( + + )} {activeView === 'model-usage' && (activeProjectId || selectedProjectId) && ( )} diff --git a/apps/frontend/src/renderer/__tests__/productivity-ui.test.ts b/apps/frontend/src/renderer/__tests__/productivity-ui.test.ts new file mode 100644 index 000000000..d427e67e5 --- /dev/null +++ b/apps/frontend/src/renderer/__tests__/productivity-ui.test.ts @@ -0,0 +1,130 @@ +/** + * Tests for the Productivity{Summary,Trends} → AnalyticsDashboard mappers. + */ + +import { describe, expect, it } from 'vitest'; +import { + buildProductivityCharts, + buildProductivityKpis, + buildProductivitySections, + formatHours, +} from '../lib/productivity-ui'; +import type { + ProductivitySummary, + ProductivityTrendPoint, +} from '../../shared/types'; + +function makeSummary(overrides: Partial = {}): ProductivitySummary { + return { + period_start: '2026-05-01T00:00:00Z', + period_end: '2026-05-22T00:00:00Z', + total_specs: 60, + completed_specs: 52, + in_progress_specs: 5, + failed_specs: 3, + total_time_saved_hours: 142.4, + total_build_time_hours: 38.7, + average_success_rate: 0.87, + first_attempt_success_rate: 0.71, + specs_by_type: {}, + specs_by_complexity: {}, + average_subtasks_per_spec: 4.33, + average_qa_iterations: 1.2, + total_subtasks_completed: 225, + specs: [], + ...overrides, + } as ProductivitySummary; +} + +const TRENDS: ProductivityTrendPoint[] = [ + { date: '2026-05-01', total_specs: 5, completed_specs: 4, time_saved_hours: 8, success_rate: 0.8 }, + { date: '2026-05-08', total_specs: 7, completed_specs: 6, time_saved_hours: 12, success_rate: 0.85 }, + { date: '2026-05-15', total_specs: 8, completed_specs: 7, time_saved_hours: 14, success_rate: 0.9 }, +]; + +const KPI_LABELS = { + specs: 'specs', + specsSub: 'period', + timeSaved: 'saved', + timeSavedSub: 'est', + successRate: 'success', + buildTime: 'build', + buildTimeSub: 'total', +}; + +describe('formatHours', () => { + it('rounds to whole hours with an h unit', () => { + expect(formatHours(52.4)).toBe('52h'); + expect(formatHours(0)).toBe('0h'); + expect(formatHours(-5)).toBe('0h'); + }); +}); + +describe('buildProductivityKpis', () => { + it('maps the summary onto four tiles, converting the 0-1 rate to a percent', () => { + const kpis = buildProductivityKpis(makeSummary(), TRENDS, KPI_LABELS); + expect(kpis).toHaveLength(4); + expect(kpis[0]).toMatchObject({ value: '52', label: 'specs' }); + expect(kpis[1].value).toBe('142h'); + expect(kpis[2]).toMatchObject({ value: '87%', tone: 'good' }); + expect(kpis[3].value).toBe('39h'); + // sparkline series come from the trends. + expect(kpis[0].trend).toEqual([4, 6, 7]); + expect(kpis[2].trend).toEqual([80, 85, 90]); + }); + + it('omits sparklines when trends are too short', () => { + const kpis = buildProductivityKpis(makeSummary(), [TRENDS[0]], KPI_LABELS); + expect(kpis[0].trend).toBeUndefined(); + }); + + it('tones a low success rate red', () => { + const kpis = buildProductivityKpis( + makeSummary({ average_success_rate: 0.4 }), + TRENDS, + KPI_LABELS, + ); + expect(kpis[2].tone).toBe('bad'); + }); +}); + +describe('buildProductivityCharts', () => { + const labels = { + velocityTitle: 'Completed', + velocityAria: 'v', + velocitySeries: 'Completed', + timeSavedTitle: 'Time saved', + timeSavedAria: 't', + timeSavedSeries: 'Hours', + }; + + it('builds velocity + time-saved charts with UTC date labels', () => { + const charts = buildProductivityCharts(TRENDS, labels, 'en-US'); + expect(charts.map((c) => c.title)).toEqual(['Completed', 'Time saved']); + expect(charts[0].series[0].values).toEqual([4, 6, 7]); + expect(charts[1].series[0].values).toEqual([8, 12, 14]); + expect(charts[0].xLabels).toEqual(['May 1', 'May 8', 'May 15']); + }); + + it('returns no charts without at least two trend points', () => { + expect(buildProductivityCharts([TRENDS[0]], labels)).toEqual([]); + }); +}); + +describe('buildProductivitySections', () => { + it('builds outcome + effort cards with one-decimal effort figures', () => { + const sections = buildProductivitySections(makeSummary(), { + outcomesTitle: 'Outcomes', + completed: 'Completed', + inProgress: 'In progress', + failed: 'Failed', + effortTitle: 'Effort', + subtasksPerSpec: 'Subtasks / spec', + qaIterations: 'QA iterations', + firstAttempt: 'First-attempt rate', + }); + expect(sections[0].rows[0]).toEqual({ label: 'Completed', value: '52' }); + expect(sections[1].rows[0]).toEqual({ label: 'Subtasks / spec', value: '4.3' }); + expect(sections[1].rows[2]).toEqual({ label: 'First-attempt rate', value: '71%' }); + }); +}); diff --git a/apps/frontend/src/renderer/components/ProductivityPilotView.tsx b/apps/frontend/src/renderer/components/ProductivityPilotView.tsx new file mode 100644 index 000000000..073999c74 --- /dev/null +++ b/apps/frontend/src/renderer/components/ProductivityPilotView.tsx @@ -0,0 +1,159 @@ +/** + * Productivity pilot on the shared design system (U5 B2). + * + * Renders `libs/ui`'s AnalyticsDashboard from the existing + * `getProductivitySummary` + `getProductivityTrends` IPC, mapped with the + * pure `productivity-ui` helpers. Read-only — the legacy Productivity view + * keeps its spec table. Reachable via the "Productivity (new UI)" sidebar + * item. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AnalyticsDashboard } from '@auto-code/ui'; +import type { UiChartCard, UiKpi, UiMetaSection } from '@auto-code/ui'; +import type { + ProductivitySummary, + ProductivityTrendPoint, +} from '../../shared/types'; +import { + buildProductivityCharts, + buildProductivityKpis, + buildProductivitySections, +} from '../lib/productivity-ui'; + +interface ProductivityPilotViewProps { + projectId: string; +} + +interface PilotState { + summary: ProductivitySummary | null; + trends: ProductivityTrendPoint[]; + loading: boolean; + error: Error | null; +} + +export function ProductivityPilotView({ + projectId, +}: Readonly) { + const { t, i18n } = useTranslation(['analytics']); + const [state, setState] = useState({ + summary: null, + trends: [], + loading: true, + error: null, + }); + const [reloadKey, setReloadKey] = useState(0); + + useEffect(() => { + let active = true; + setState({ summary: null, trends: [], loading: true, error: null }); + Promise.all([ + window.electronAPI.getProductivitySummary(projectId), + window.electronAPI.getProductivityTrends(projectId), + ]) + .then(([summaryResult, trendsResult]) => { + if (!active) return; + if (!summaryResult.success || summaryResult.data == null) { + console.error( + '[ProductivityPilotView] Failed to load summary:', + summaryResult.success ? 'no data' : summaryResult.error, + ); + setState({ + summary: null, + trends: [], + loading: false, + error: new Error(t('analytics:productivityPilot.error')), + }); + return; + } + setState({ + summary: summaryResult.data, + trends: trendsResult.success ? (trendsResult.data ?? []) : [], + loading: false, + error: null, + }); + }) + .catch((err: unknown) => { + if (!active) return; + console.error('[ProductivityPilotView] Failed to load productivity:', err); + setState({ + summary: null, + trends: [], + loading: false, + error: new Error(t('analytics:productivityPilot.error')), + }); + }); + return () => { + active = false; + }; + }, [projectId, reloadKey, t]); + + const reload = useCallback(() => setReloadKey((key) => key + 1), []); + + const kpis = useMemo(() => { + if (state.summary == null) return null; + return buildProductivityKpis(state.summary, state.trends, { + specs: t('analytics:productivityPilot.kpis.specs'), + specsSub: t('analytics:productivityPilot.kpis.specsSub'), + timeSaved: t('analytics:productivityPilot.kpis.timeSaved'), + timeSavedSub: t('analytics:productivityPilot.kpis.timeSavedSub'), + successRate: t('analytics:productivityPilot.kpis.successRate'), + buildTime: t('analytics:productivityPilot.kpis.buildTime'), + buildTimeSub: t('analytics:productivityPilot.kpis.buildTimeSub'), + }); + }, [state.summary, state.trends, t]); + + const charts = useMemo(() => { + if (state.summary == null) return undefined; + return buildProductivityCharts( + state.trends, + { + velocityTitle: t('analytics:productivityPilot.charts.velocityTitle'), + velocityAria: t('analytics:productivityPilot.charts.velocityAria'), + velocitySeries: t('analytics:productivityPilot.charts.velocitySeries'), + timeSavedTitle: t('analytics:productivityPilot.charts.timeSavedTitle'), + timeSavedAria: t('analytics:productivityPilot.charts.timeSavedAria'), + timeSavedSeries: t('analytics:productivityPilot.charts.timeSavedSeries'), + }, + i18n.language, + ); + }, [state.summary, state.trends, t, i18n.language]); + + const sections = useMemo(() => { + if (state.summary == null) return undefined; + return buildProductivitySections(state.summary, { + outcomesTitle: t('analytics:productivityPilot.sections.outcomesTitle'), + completed: t('analytics:productivityPilot.sections.completed'), + inProgress: t('analytics:productivityPilot.sections.inProgress'), + failed: t('analytics:productivityPilot.sections.failed'), + effortTitle: t('analytics:productivityPilot.sections.effortTitle'), + subtasksPerSpec: t('analytics:productivityPilot.sections.subtasksPerSpec'), + qaIterations: t('analytics:productivityPilot.sections.qaIterations'), + firstAttempt: t('analytics:productivityPilot.sections.firstAttempt'), + }); + }, [state.summary, t]); + + const stateLabels = useMemo( + () => ({ + loading: t('analytics:productivityPilot.states.loading'), + retry: t('analytics:productivityPilot.states.retry'), + empty: t('analytics:productivityPilot.states.empty'), + }), + [t], + ); + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/src/renderer/components/Sidebar.tsx b/apps/frontend/src/renderer/components/Sidebar.tsx index e6dd01d30..771156d71 100644 --- a/apps/frontend/src/renderer/components/Sidebar.tsx +++ b/apps/frontend/src/renderer/components/Sidebar.tsx @@ -71,7 +71,7 @@ import { SessionContextIndicator } from './SessionContextIndicator'; import { NavIndicator } from './NavIndicator'; import type { Project, AutoBuildVersionInfo, GitStatus, ProjectEnvConfig } from '../../shared/types'; -export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'merge-analytics' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector'; +export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector'; interface SidebarProps { onSettingsClick: () => void; @@ -103,6 +103,7 @@ const baseNavItems: NavItem[] = [ { id: 'analytics', labelKey: 'navigation:items.analytics', icon: BarChart3, shortcut: 'Y' }, { id: 'analytics-next', labelKey: 'navigation:items.analyticsNext', icon: BarChart3 }, { id: 'productivity', labelKey: 'navigation:items.productivity', icon: TrendingUp, shortcut: 'P' }, + { id: 'productivity-next', labelKey: 'navigation:items.productivityNext', icon: Activity }, { id: 'patterns', labelKey: 'navigation:items.patterns', icon: Code, shortcut: 'Z' }, { id: 'patterns-next', labelKey: 'navigation:items.patternsNext', icon: Code }, { id: 'agent-tools', labelKey: 'navigation:items.agentTools', icon: Wrench, shortcut: 'M' }, diff --git a/apps/frontend/src/renderer/lib/productivity-ui.ts b/apps/frontend/src/renderer/lib/productivity-ui.ts new file mode 100644 index 000000000..fc492d832 --- /dev/null +++ b/apps/frontend/src/renderer/lib/productivity-ui.ts @@ -0,0 +1,170 @@ +/** + * Pure mapping helpers between the ProductivitySummary/Trends IPC shapes and + * the shared AnalyticsDashboard screen. The mockup's categorical bar panels + * ("when you ship", "where the time went") have no backing data, so — like + * the other pilots — this maps only what the backend actually provides: KPI + * summary figures plus the daily trend series as line charts. + */ + +import type { UiChartCard, UiKpi, UiMetaSection } from '@auto-code/ui'; +import type { + ProductivitySummary, + ProductivityTrendPoint, +} from '../../shared/types'; +import { + formatPercent, + sampleLabels, + shortDate, + successRateTone, +} from './analytics-ui'; + +/** Whole hours with an "h" unit, e.g. 52.4 → "52h". */ +export function formatHours(hours: number): string { + if (!Number.isFinite(hours) || hours < 0) return '0h'; + return `${Math.round(hours)}h`; +} + +export interface ProductivityKpiLabels { + specs: string; + specsSub: string; + timeSaved: string; + timeSavedSub: string; + successRate: string; + buildTime: string; + buildTimeSub: string; +} + +export function buildProductivityKpis( + summary: ProductivitySummary, + trends: readonly ProductivityTrendPoint[], + labels: ProductivityKpiLabels, +): UiKpi[] { + // Backend success rates are 0.0–1.0; the tile shows a whole percent. + const ratePct = summary.average_success_rate * 100; + const specSeries = trends.map((point) => point.completed_specs); + const savedSeries = trends.map((point) => point.time_saved_hours); + const rateSeries = trends.map((point) => point.success_rate * 100); + const rateTone = successRateTone(ratePct); + return [ + { + value: String(summary.completed_specs), + label: labels.specs, + sub: labels.specsSub, + trend: specSeries.length >= 2 ? specSeries : undefined, + trendTone: 'info', + }, + { + value: formatHours(summary.total_time_saved_hours), + label: labels.timeSaved, + sub: labels.timeSavedSub, + trend: savedSeries.length >= 2 ? savedSeries : undefined, + trendTone: 'good', + }, + { + value: formatPercent(ratePct), + label: labels.successRate, + tone: rateTone, + trend: rateSeries.length >= 2 ? rateSeries : undefined, + trendTone: rateTone === 'bad' ? 'bad' : 'good', + }, + { + value: formatHours(summary.total_build_time_hours), + label: labels.buildTime, + sub: labels.buildTimeSub, + }, + ]; +} + +export interface ProductivityChartLabels { + velocityTitle: string; + velocityAria: string; + velocitySeries: string; + timeSavedTitle: string; + timeSavedAria: string; + timeSavedSeries: string; +} + +export function buildProductivityCharts( + trends: readonly ProductivityTrendPoint[], + labels: ProductivityChartLabels, + locale?: string, +): UiChartCard[] { + if (trends.length < 2) return []; + const xLabels = sampleLabels(trends.map((p) => shortDate(p.date, locale))); + return [ + { + title: labels.velocityTitle, + ariaLabel: labels.velocityAria, + xLabels, + series: [ + { + label: labels.velocitySeries, + tone: 'info', + values: trends.map((p) => p.completed_specs), + }, + ], + }, + { + title: labels.timeSavedTitle, + ariaLabel: labels.timeSavedAria, + xLabels, + series: [ + { + label: labels.timeSavedSeries, + tone: 'good', + values: trends.map((p) => p.time_saved_hours), + }, + ], + }, + ]; +} + +export interface ProductivitySectionLabels { + outcomesTitle: string; + completed: string; + inProgress: string; + failed: string; + effortTitle: string; + subtasksPerSpec: string; + qaIterations: string; + firstAttempt: string; +} + +/** Round to at most one decimal, dropping a trailing ".0". */ +function oneDecimal(value: number): string { + if (!Number.isFinite(value)) return '0'; + return String(Math.round(value * 10) / 10); +} + +export function buildProductivitySections( + summary: ProductivitySummary, + labels: ProductivitySectionLabels, +): UiMetaSection[] { + return [ + { + title: labels.outcomesTitle, + rows: [ + { label: labels.completed, value: String(summary.completed_specs) }, + { label: labels.inProgress, value: String(summary.in_progress_specs) }, + { label: labels.failed, value: String(summary.failed_specs) }, + ], + }, + { + title: labels.effortTitle, + rows: [ + { + label: labels.subtasksPerSpec, + value: oneDecimal(summary.average_subtasks_per_spec), + }, + { + label: labels.qaIterations, + value: oneDecimal(summary.average_qa_iterations), + }, + { + label: labels.firstAttempt, + value: formatPercent(summary.first_attempt_success_rate * 100), + }, + ], + }, + ]; +} diff --git a/apps/frontend/src/shared/i18n/locales/en/analytics.json b/apps/frontend/src/shared/i18n/locales/en/analytics.json index cf9364944..ae50298d0 100644 --- a/apps/frontend/src/shared/i18n/locales/en/analytics.json +++ b/apps/frontend/src/shared/i18n/locales/en/analytics.json @@ -105,5 +105,40 @@ "approved": "Approved", "rejectionRate": "Rejection rate" } + }, + "productivityPilot": { + "error": "Failed to load productivity data.", + "states": { + "loading": "Loading productivity…", + "retry": "Retry", + "empty": "No productivity data yet." + }, + "kpis": { + "specs": "specs completed", + "specsSub": "this period", + "timeSaved": "time saved", + "timeSavedSub": "estimated", + "successRate": "success rate", + "buildTime": "build time", + "buildTimeSub": "total" + }, + "charts": { + "velocityTitle": "Completed specs", + "velocityAria": "Completed specs per day over the period", + "velocitySeries": "Completed", + "timeSavedTitle": "Time saved", + "timeSavedAria": "Hours saved per day over the period", + "timeSavedSeries": "Hours saved" + }, + "sections": { + "outcomesTitle": "Outcomes", + "completed": "Completed", + "inProgress": "In progress", + "failed": "Failed", + "effortTitle": "Effort", + "subtasksPerSpec": "Subtasks / spec", + "qaIterations": "QA iterations", + "firstAttempt": "First-attempt rate" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json index 78584fd9e..00f28413d 100644 --- a/apps/frontend/src/shared/i18n/locales/en/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json @@ -34,7 +34,8 @@ "githubPRsNext": "GitHub PRs (New UI)", "githubIssuesNext": "GitHub Issues (New UI)", "patternsNext": "Patterns (New UI)", - "analyticsNext": "Analytics (New UI)" + "analyticsNext": "Analytics (New UI)", + "productivityNext": "Productivity (New UI)" }, "actions": { "settings": "Settings", diff --git a/apps/frontend/src/shared/i18n/locales/fr/analytics.json b/apps/frontend/src/shared/i18n/locales/fr/analytics.json index 2b054b63d..a10d9190f 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/analytics.json +++ b/apps/frontend/src/shared/i18n/locales/fr/analytics.json @@ -105,5 +105,40 @@ "approved": "Approuvées", "rejectionRate": "Taux de rejet" } + }, + "productivityPilot": { + "error": "Échec du chargement des données de productivité.", + "states": { + "loading": "Chargement de la productivité…", + "retry": "Réessayer", + "empty": "Aucune donnée de productivité pour le moment." + }, + "kpis": { + "specs": "spécs terminées", + "specsSub": "cette période", + "timeSaved": "temps gagné", + "timeSavedSub": "estimé", + "successRate": "taux de réussite", + "buildTime": "temps de build", + "buildTimeSub": "total" + }, + "charts": { + "velocityTitle": "Spécs terminées", + "velocityAria": "Spécs terminées par jour sur la période", + "velocitySeries": "Terminées", + "timeSavedTitle": "Temps gagné", + "timeSavedAria": "Heures gagnées par jour sur la période", + "timeSavedSeries": "Heures gagnées" + }, + "sections": { + "outcomesTitle": "Résultats", + "completed": "Terminées", + "inProgress": "En cours", + "failed": "Échouées", + "effortTitle": "Effort", + "subtasksPerSpec": "Sous-tâches / spéc", + "qaIterations": "Itérations QA", + "firstAttempt": "Taux au 1er essai" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json index c2ce10a78..ea7a36f05 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json @@ -34,7 +34,8 @@ "githubPRsNext": "PRs GitHub (nouvelle interface)", "githubIssuesNext": "Issues GitHub (nouvelle interface)", "patternsNext": "Patterns (nouvelle interface)", - "analyticsNext": "Analytique (nouvelle interface)" + "analyticsNext": "Analytique (nouvelle interface)", + "productivityNext": "Productivité (nouvelle interface)" }, "actions": { "settings": "Paramètres",