Skip to content
Open
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
8 changes: 8 additions & 0 deletions apps/manager/src/features/coverage/coverage-report-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,13 @@ export function CoverageReportClient({
)
const selectedVideoCount = selectedVideoIds.size
const selectedLanguageCount = selectedLanguageIds.length
const selectedLanguageQuery = useMemo(() => {
if (selectedLanguageIds.length === 0) return ""

const params = new URLSearchParams()
params.set("languageId", selectedLanguageIds.join(","))
return params.toString()
}, [selectedLanguageIds])
const languageSelectionRequired = requiresLanguageSelectionForEnrich(
selectedVideoCount,
selectedLanguageCount,
Expand Down Expand Up @@ -1731,6 +1738,7 @@ export function CoverageReportClient({
languageSelectionRequired={languageSelectionRequired}
onCancel={handleCancelEnrichSelection}
onEnrich={handleEnrichSelection}
reportQuery={selectedLanguageQuery}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ describe("EnrichActionControls", () => {
languageSelectionRequired: false,
onCancel: vi.fn(),
onEnrich: vi.fn(),
reportQuery: "languageId=529",
}),
)

expect(markup).toContain("1 enrichment job started.")
expect(markup).toContain('href="/dashboard/jobs/job-1"')
expect(markup).toContain('href="/dashboard/jobs/job-1?languageId=529"')
expect(markup).toContain("Open job")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from "react"

import type { EnrichFeedback } from "@/features/enrich-selection"
import { buildDashboardHrefWithReportQuery } from "@/features/nav/dashboard-nav-model"

type EnrichActionControlsProps = {
enrichActionReady: boolean
Expand All @@ -11,6 +12,7 @@ type EnrichActionControlsProps = {
languageSelectionRequired: boolean
onCancel: () => void
onEnrich: () => void | Promise<void>
reportQuery?: string
}

export function EnrichActionControls({
Expand All @@ -20,8 +22,12 @@ export function EnrichActionControls({
languageSelectionRequired,
onCancel,
onEnrich,
reportQuery = "",
}: EnrichActionControlsProps) {
const actionDisabled = !enrichActionReady || isEnrichSubmitting
const feedbackActionHref = enrichFeedback?.action
? buildDashboardHrefWithReportQuery(enrichFeedback.action.href, reportQuery)
: null

return (
<div className="translation-controls">
Expand Down Expand Up @@ -87,7 +93,7 @@ export function EnrichActionControls({
{" "}
<a
className="translation-feedback-action"
href={enrichFeedback.action.href}
href={feedbackActionHref ?? enrichFeedback.action.href}
>
{enrichFeedback.action.label}
</a>
Expand Down
60 changes: 60 additions & 0 deletions apps/manager/src/features/nav/dashboard-nav-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest"

import {
buildDashboardHrefWithReportQuery,
buildDashboardNavHref,
} from "./dashboard-nav-model"

describe("dashboard nav model", () => {
it("carries the canonical report language query across dashboard tabs", () => {
expect(buildDashboardNavHref("/dashboard/jobs", "languageId=529")).toBe(
"/dashboard/jobs?languageId=529",
)

expect(buildDashboardNavHref("/dashboard/coverage", "languageId=529")).toBe(
"/dashboard/coverage?languageId=529",
)

expect(buildDashboardNavHref("/dashboard/agents", "languageId=529")).toBe(
"/dashboard/agents?languageId=529",
)
})

it("canonicalizes legacy languageIds query params", () => {
expect(
buildDashboardNavHref("/dashboard/agents", "languageIds=529,21028"),
).toBe("/dashboard/agents?languageId=529%2C21028")
})

it("carries the report language query to dashboard job detail handoffs", () => {
expect(
buildDashboardHrefWithReportQuery(
"/dashboard/jobs/job-1",
"languageIds=529,21028",
),
).toBe("/dashboard/jobs/job-1?languageId=529%2C21028")
})

it("drops unsupported query params instead of carrying hidden dashboard state", () => {
expect(
buildDashboardNavHref(
"/dashboard/coverage",
"languageId=529&refresh=1&status=failed",
),
).toBe("/dashboard/coverage?languageId=529")

expect(buildDashboardNavHref("/dashboard/jobs", "status=failed")).toBe(
"/dashboard/jobs",
)
})

it("returns bare tab paths when no report language is selected", () => {
expect(buildDashboardNavHref("/dashboard/coverage", "")).toBe(
"/dashboard/coverage",
)

expect(buildDashboardNavHref("/dashboard/jobs", "languageId=")).toBe(
"/dashboard/jobs",
)
})
})
37 changes: 37 additions & 0 deletions apps/manager/src/features/nav/dashboard-nav-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Route } from "next"
import { resolveRequestedLanguageIds } from "@/features/coverage/language-selection"

export type DashboardNavPath =
| "/dashboard/coverage"
| "/dashboard/jobs"
| "/dashboard/agents"

export type DashboardReportQueryPath =
| DashboardNavPath
| `/dashboard/jobs/${string}`

export function buildDashboardHrefWithReportQuery<
TPath extends DashboardReportQueryPath,
>(pathname: TPath, currentQuery: string): Route<TPath> {
const params = new URLSearchParams(currentQuery)
const languageIds = resolveRequestedLanguageIds({
languageId: params.get("languageId") ?? undefined,
languageIds: params.get("languageIds") ?? undefined,
})

if (languageIds.length === 0) {
return pathname as Route<TPath>
}

const nextParams = new URLSearchParams()
nextParams.set("languageId", languageIds.join(","))

return `${pathname}?${nextParams.toString()}` as Route<TPath>
}

export function buildDashboardNavHref<TPath extends DashboardNavPath>(
pathname: TPath,
currentQuery: string,
): Route<TPath> {
return buildDashboardHrefWithReportQuery(pathname, currentQuery)
}
17 changes: 13 additions & 4 deletions apps/manager/src/features/nav/dashboard-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client"

import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { BarChart2, Bot, ListChecks, LogOut } from "lucide-react"
import { apiFetch } from "@/lib/api-fetch"
import { buildDashboardNavHref } from "./dashboard-nav-model"

type NavUser = { username: string; email: string }

Expand All @@ -20,6 +21,7 @@ function getInitials(username: string): string {
export function DashboardNav({ user }: { user: NavUser }) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [queueCount, setQueueCount] = useState<number | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
Expand All @@ -28,6 +30,13 @@ export function DashboardNav({ user }: { user: NavUser }) {
const isJobs =
pathname.startsWith("/dashboard/jobs") || pathname === "/dashboard"
const isAgents = pathname.startsWith("/dashboard/agents")
const currentQuery = searchParams.toString()
const coverageHref = buildDashboardNavHref(
"/dashboard/coverage",
currentQuery,
)
const jobsHref = buildDashboardNavHref("/dashboard/jobs", currentQuery)
const agentsHref = buildDashboardNavHref("/dashboard/agents", currentQuery)

useEffect(() => {
let cancelled = false
Expand Down Expand Up @@ -72,7 +81,7 @@ export function DashboardNav({ user }: { user: NavUser }) {
return (
<nav className="header-diagram-menu header-nav-tabs">
<Link
href="/dashboard/coverage"
href={coverageHref}
className={`header-nav-link${isCoverage ? " is-active" : ""}`}
{...(isCoverage ? { "aria-current": "page" as const } : {})}
>
Expand All @@ -82,7 +91,7 @@ export function DashboardNav({ user }: { user: NavUser }) {
<span>Report</span>
</Link>
<Link
href="/dashboard/jobs"
href={jobsHref}
className={`header-nav-link${isJobs ? " is-active" : ""}`}
{...(isJobs ? { "aria-current": "page" as const } : {})}
>
Expand All @@ -101,7 +110,7 @@ export function DashboardNav({ user }: { user: NavUser }) {
)}
</Link>
<Link
href="/dashboard/agents"
href={agentsHref}
className={`header-nav-link${isAgents ? " is-active" : ""}`}
{...(isAgents ? { "aria-current": "page" as const } : {})}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
date: 2026-04-13
topic: manager-report-filter-restoration
related:
- docs/roadmap/media-generation/feat-030-video-content-discovery-dashboard.md
- docs/roadmap/media-generation/feat-084-manager-agents-automations.md
- docs/roadmap/media-generation/feat-084-enrich-now-feedback.md
- docs/solutions/integration-issues/manager-coverage-language-first-empty-state-20260410.md
- docs/solutions/ui-bugs/manager-enrich-now-feedback-handoff-20260413.md
- docs/solutions/integration-issues/manager-coverage-dashboard-review-regression-cleanup.md
---

# Manager Report Filter Restoration

## What We're Building

When an operator opens the Manager Report coverage page with URL-backed filters
selected, such as `/dashboard/coverage?languageId=529`, then moves to Jobs or
Agents and returns to Report, the Report page should reopen with the same
selection instead of dropping back to `/dashboard/coverage`.

The important behavior is continuity: Report is the operator's working context,
and Jobs or Agents are supporting screens. A quick check of job progress or
automation settings should not make the operator rebuild the language selection
they already made.

## Requirements

- R1. Starting from `/dashboard/coverage?languageId=529`, navigating to Jobs and
then returning to Report restores `/dashboard/coverage?languageId=529`.
- R2. Starting from `/dashboard/coverage?languageId=529`, navigating to Agents
and then returning to Report restores `/dashboard/coverage?languageId=529`.
- R3. The behavior applies to URL-backed Report filters, with `languageId` as
the canonical current example.
- R4. Legacy `languageIds` links should still be accepted, but return paths
should prefer the canonical `languageId` shape where the route is rewritten.
- R5. Direct navigation to `/dashboard/coverage` without a prior URL-backed
Report context still opens the default language-first Report state.
- R6. Clearing the Report language selection should clear the carried Report
filter context rather than keeping a stale language active.

## Why This Approach

The recommended approach is to preserve URL-backed Report state through the
Manager dashboard navigation and Report-originated handoffs. This keeps the
selection visible in the URL, matches the existing coverage filter contract, and
avoids inventing a hidden cross-page state store for a URL-shaped problem.

We considered three options:

- Carry the Report query through dashboard navigation. This is transparent,
matches the user's URL example, and keeps Jobs and Agents as temporary stops
without losing the Report context.
- Remember the last Report URL in browser session storage. This would avoid
query strings on Jobs and Agents, but it would make the return behavior hidden
and easier to confuse with direct links.
- Promote every local Report control to URL state. This could eventually cover
search text, collection type, coverage segment, and report type, but it is
broader than the reported bug. V1 should only preserve filters that are
already URL-backed or intentionally promoted during planning.

## Key Decisions

- Preserve URL-backed Report selections across Report -> Jobs -> Report and
Report -> Agents -> Report loops.
- Treat `languageId` as the canonical query key while continuing to accept
legacy `languageIds` input.
- Keep the behavior scoped to Manager dashboard navigation and Report-originated
handoffs; do not change CMS coverage queries or coverage aggregation.
- Do not persist unrelated local UI controls unless planning explicitly decides
to make them URL-backed filters.
- Let explicit URLs win. If the user opens `/dashboard/coverage` directly, that
should remain the default state unless they arrived through a carried Report
context.

## Success Criteria

- From `/dashboard/coverage?languageId=529`, click Jobs, then click Report: the
language selection for `529` is restored and the URL includes `languageId=529`.
- From `/dashboard/coverage?languageId=529`, click Agents, then click Report: the
language selection for `529` is restored and the URL includes `languageId=529`.
- From `/dashboard/coverage?languageIds=529,21028`, the Report selection still
loads, and any rewritten return URL uses `languageId=529%2C21028` or the
project-standard equivalent.
- From `/dashboard/coverage`, Report still opens the language-first empty state.
- Removing the selected language and leaving Report does not revive a stale
language when returning.

## Scope Boundaries

- This is a Manager UI navigation-state fix, not a CMS data or coverage-query
change.
- Do not redesign the Report, Jobs, or Agents screens.
- Do not add global app state for every dashboard filter.
- Do not change the coverage API request shape beyond whatever is needed to
preserve the existing URL-backed filter contract.

## Resolved Questions

- The first filter to preserve is the existing language selection represented by
`languageId`.
- The route should remain shareable and understandable by keeping the selection
visible in URL query parameters.
- Existing query normalization in `language-selection.ts` is the contract to
respect during planning.
- Jobs and Agents can ignore the carried Report query if they do not need it;
the user-facing requirement is that returning to Report restores the selection.

## Open Questions

No product-level blockers remain for planning.

## Next Steps

Proceed to `/workflows:plan` for a narrow Manager navigation-state fix. The
likely entry points are `apps/manager/src/features/nav/dashboard-nav.tsx`,
`apps/manager/src/features/coverage/language-selection.ts`, and the existing
Coverage-to-Jobs handoff paths in `apps/manager/src/features/coverage/`.
Loading
Loading