Skip to content

feat: tracked-in field for failures, fix dashboard filter truncation (#155, #156)#160

Open
myakove wants to merge 12 commits into
mainfrom
feat/issue-155-156-tracked-in-dashboard
Open

feat: tracked-in field for failures, fix dashboard filter truncation (#155, #156)#160
myakove wants to merge 12 commits into
mainfrom
feat/issue-155-156-tracked-in-dashboard

Conversation

@myakove

@myakove myakove commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Two features:

  1. "Tracked In" field (feat: "Tracked In" field for failed tests — link Jira/GitHub issues to failure history #155): Failed tests can now be linked to Jira/GitHub issues. Links are set automatically when creating issues via the app, or manually by users. Visible in the job results page (badge + dialog), CLI, and API.

  2. Dashboard filter fix (fix: dashboard filters and search are applied after 500-job truncation — older jobs are invisible #156): Dashboard had a hardcoded LIMIT 500 applied before filtering, making older jobs invisible. Now all filters (search, status, date, metadata) are pushed into SQL so the LIMIT applies to the filtered set. Server-side pagination replaces client-side slicing.

Closes #155
Closes #156

Changes

Issue #155

  • Add tracked_in_url and tracked_in_type columns to failure_history
  • Auto-set tracked-in after Jira/GitHub issue creation
  • PUT /results/{job_id}/tracked-in and GET /results/{job_id}/tracked-in endpoints
  • Include tracked-in data in result responses and history endpoints
  • Frontend: TrackedInBadge (icon + link) and TrackInDialog (manual URL entry)
  • CLI: rootcoz results set-tracked-in command + display in results show

Issue #156

  • list_results_for_dashboard_filtered() — filters in SQL before LIMIT
  • /api/dashboard supports limit/offset, /api/dashboard/filtered supports search/status/date_from/date_to/limit/offset
  • Frontend: server-side pagination with debounced search
  • Extracted _parse_dashboard_row and _DASHBOARD_BASE_SQL to avoid duplication

All 2590 tests pass (2339 backend + 251 frontend).

@myakove-bot

Copy link
Copy Markdown
Collaborator

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message
  • /security-override - Set security check runs to pass (maintainers only)
  • /security-override cancel - Re-run security checks

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest python-module-install - Test Python package installation
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3
  • /cherry-pick-retry <branch> - Retry a failed cherry-pick (merged PRs only)

Branch Management

  • /rebase - Rebase this PR branch onto its base branch

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. Status Checks: All required status checks must pass
  3. No Blockers: No wip, hold, has-conflicts labels and PR must be mergeable (no conflicts)
  4. Verified: PR must be marked as verified

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove

Reviewers:

  • myakove
  • rnetser
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge
AI Features
  • Conventional Title: Mode: fix (claude/claude-opus-4-6-1m)
  • Cherry-Pick Conflict Resolution: Enabled (claude/claude-opus-4-6-1m)
Security Checks
  • Suspicious Path Detection: Monitors paths: .claude/, .vscode/, .cursor/, .devcontainer/, .pi/, .github/workflows/, .github/actions/
  • Committer Identity Check: Verifies last committer matches PR author
  • Mandatory: Security checks block merge (use /security-override to bypass — maintainers only)

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is removed on new commits unless the push is detected as a clean rebase
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add tracked-in links for failures and fix dashboard filtering/pagination

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add per-failure “Tracked In” links (API/CLI/UI) for Jira/GitHub issue tracking.
• Fix dashboard truncation by applying filters in SQL before LIMIT and paginating server-side.
• Refactor dashboard query parsing to reduce duplication and improve maintainability.
Diagram

graph TD
FE_Dash["Dashboard UI"] --> API["Rootcoz API"] --> Store["Storage (SQL filters)"] --> DB[(SQLite)]
FE_Report["Report UI"] --> API --> Store --> DB
CLI["rootcoz CLI"] --> API --> Store --> DB
Issue["Issue creation"] --> EXT{{"Jira/GitHub"}} --> API --> Store --> DB

subgraph Legend
direction LR
_fe["Client/UI"] ~~~ _api["API"] ~~~ _db[(Database)] ~~~ _ext{{"External"}}
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate tracked-in table (failure_tracking)
  • ➕ Allows multiple links per failure (e.g., Jira + GitHub) and richer metadata (status, key).
  • ➕ Avoids widening failure_history and keeps historical schema simpler.
  • ➖ Adds join complexity to common history/result reads.
  • ➖ Requires new migration + more code paths vs two columns on existing row.
2. Keyset pagination for dashboard (created_at/job_id cursor)
  • ➕ Avoids OFFSET performance issues on large datasets.
  • ➕ More stable pagination under concurrent inserts.
  • ➖ More complex API contract for clients than limit/offset.
  • ➖ Requires careful ordering + cursor encoding and testing.
3. Store tracked-in solely as comments/annotations
  • ➕ No schema change needed; reuses existing comment model.
  • ➕ Flexible text-based linking.
  • ➖ Harder to query/display as a first-class field.
  • ➖ Ambiguous semantics and harder to enforce per-test uniqueness.

Recommendation: Current approach is appropriate: adding tracked_in_url/type to failure_history is the simplest way to make tracked-in a first-class, queryable attribute, and SQL-first filtering with limit/offset immediately resolves the truncation bug. If the dashboard dataset grows significantly, consider a follow-up moving to keyset pagination; if tracked-in needs multiple references or richer lifecycle state, consider extracting to a dedicated tracking table later.

Files changed (14) +738 / -161

Enhancement (10) +645 / -91
ReportPage.tsxLoad tracked-in data from result response into report state +8/-0

Load tracked-in data from result response into report state

• Consumes the new 'tracked_in' payload from 'GET /results/{jobId}' and stores it in the report context. Ensures tracked-in links remain available after initial load and during refreshes.

frontend/src/pages/ReportPage.tsx

FailureCard.tsxDisplay tracked-in badges and add manual Track In dialog +25/-3

Display tracked-in badges and add manual Track In dialog

• Shows a per-failure tracked-in badge when a link exists and adds a "Track In..." action for non-viewers to manually link an issue. Also auto-updates local tracked-in state after Jira/GitHub issue creation to match backend persistence.

frontend/src/pages/report/FailureCard.tsx

ReportContext.tsxAdd tracked-in state/actions to report reducer +10/-1

Add tracked-in state/actions to report reducer

• Introduces 'trackedIn' keyed by test_name plus reducer actions to set the full map or update an individual entry. Enables consistent tracked-in rendering across report components.

frontend/src/pages/report/ReportContext.tsx

TrackedInBadge.tsxNew tracked-in badge + dialog component with URL type detection +144/-0

New tracked-in badge + dialog component with URL type detection

• Adds UI components to render a compact tracker badge (GitHub/Jira/unknown) and a dialog for manually setting tracked-in URLs. The dialog calls 'PUT /results/{jobId}/tracked-in', handles errors, and updates report state on success.

frontend/src/pages/report/TrackedInBadge.tsx

index.tsAdd tracked-in types to ResultResponse contract +7/-0

Add tracked-in types to ResultResponse contract

• Defines 'TrackedInEntry' and extends 'ResultResponse' with optional 'tracked_in' mapping (test_name → tracked_in_url/type). Aligns frontend typing with the backend response.

frontend/src/types/index.ts

client.pySupport dashboard pagination/filters and add tracked-in client methods +38/-3

Support dashboard pagination/filters and add tracked-in client methods

• Extends dashboard endpoints to accept limit/offset and adds search/status/date filters for dashboard_filtered, returning '{jobs, total}'. Adds 'set_tracked_in' and 'get_tracked_in' methods for the new tracked-in API endpoints.

src/rootcoz/cli/client.py

main.pyAdd 'results set-tracked-in' and show tracked-in in 'results show' +52/-8

Add 'results set-tracked-in' and show tracked-in in 'results show'

• Adds a new CLI subcommand to set/clear tracked-in URLs per test failure and prints a friendly confirmation. Updates 'results dashboard' to accept '--search' and '--limit', and prints tracked-in mappings when present in 'results show'.

src/rootcoz/cli/main.py

main.pyExpose tracked-in on results; add tracked-in endpoints; paginate dashboard APIs +120/-21

Expose tracked-in on results; add tracked-in endpoints; paginate dashboard APIs

• Attaches per-job tracked-in data to 'GET /results/{job_id}' and auto-persists tracked-in after Jira/GitHub issue creation. Adds 'PUT/GET /results/{job_id}/tracked-in' endpoints with type auto-detection, and updates '/api/dashboard' + '/api/dashboard/filtered' to support limit/offset and return '{jobs, total}' for pagination.

src/rootcoz/main.py

models.pyAdd SetTrackedInRequest model with validation +24/-0

Add SetTrackedInRequest model with validation

• Introduces a Pydantic model for setting/clearing tracked-in URLs including validation/normalization for URL and tracker type values.

src/rootcoz/models.py

storage.pyPersist tracked-in fields and implement SQL-first dashboard filtering +217/-55

Persist tracked-in fields and implement SQL-first dashboard filtering

• Adds migrations for 'failure_history.tracked_in_url' and 'tracked_in_type' plus storage helpers to set and fetch tracked-in data per job. Refactors dashboard row parsing into a helper and introduces 'list_results_for_dashboard_filtered()' that applies filters in SQL before LIMIT and returns total counts for pagination.

src/rootcoz/storage.py

Bug fix (1) +42 / -32
DashboardPage.tsxMove dashboard filtering/pagination to backend with debounced search +42/-32

Move dashboard filtering/pagination to backend with debounced search

• Switches dashboard data loading to pass search/status/date filters and limit/offset to '/api/dashboard/filtered'. Adds debounced search to reduce request volume, tracks total rows from the backend, and removes client-side slicing so pagination reflects the full filtered set.

frontend/src/pages/DashboardPage.tsx

Tests (3) +51 / -38
test_cli_main.pyUpdate CLI dashboard tests for new params/return shapes +31/-25

Update CLI dashboard tests for new params/return shapes

• Adjusts dashboard CLI tests to expect limit defaulting and 'dashboard_filtered' returning '{jobs, total}'. Verifies the new filtered invocation includes search/limit arguments.

tests/test_cli_main.py

test_job_metadata.pyUpdate dashboard filtered API tests for '{jobs,total}' response +11/-8

Update dashboard filtered API tests for '{jobs,total}' response

• Updates assertions to match the new response structure and validates both fields exist. Adjusts job-name extraction to read from 'data['jobs']'.

tests/test_job_metadata.py

test_main.pyUpdate API tests for dashboard pagination and filtered response shape +9/-5

Update API tests for dashboard pagination and filtered response shape

• Updates expectations for '/api/dashboard' calling storage with '(limit, offset)' and modifies dashboard filtered tests to read job lists from 'data['jobs']'. Keeps existing include/exclude semantics coverage while adapting to pagination-ready responses.

tests/test_main.py

@qodo-code-review

qodo-code-review Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 40 rules

Grey Divider


Action required

1. Tracked-in auto-set unscoped ✓ Resolved 🐞 Bug ≡ Correctness
Description
_add_tracker_comment() calls storage.set_tracked_in() without passing
child_job_name/child_build_number, so it can update multiple failure_history rows when the same
test_name exists across child jobs/builds.
This can mis-associate Jira/GitHub issues to the wrong failures (data integrity bug).
Code

src/rootcoz/main.py[R5246-5251]

+    # Auto-set tracked-in on the failure_history row
+    if tracked_type and issue_url:
+        try:
+            await storage.set_tracked_in(
+                job_id, body.test_name, issue_url, tracked_type
+            )
Relevance

⭐⭐⭐ High

Child-scope data-integrity issues have been repeatedly accepted (child scoping fixes) in PRs #59 and
#146.

PR-#59
PR-#146

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The auto-set logic calls storage.set_tracked_in() without child scoping, even though the request
body includes child fields (used for comment insertion). The storage layer only narrows the UPDATE
to a specific child row when child_job_name/child_build_number are provided, and failure_history
is explicitly keyed/indexed by these columns, meaning duplicates across children are expected and
supported.

src/rootcoz/main.py[5246-5251]
src/rootcoz/main.py[5224-5233]
src/rootcoz/storage.py[2039-2072]
src/rootcoz/storage.py[513-547]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When creating a Jira/GitHub issue, `_add_tracker_comment()` attempts to auto-set the tracked-in URL in `failure_history`, but it calls `storage.set_tracked_in(job_id, test_name, ...)` without the child scoping fields. If a job has multiple failures with the same `test_name` across child jobs/builds, this update can affect the wrong row(s).

### Issue Context
- `CreateIssueRequest` already carries `child_job_name` / `child_build_number` and `_add_tracker_comment()` uses them when adding the comment.
- `failure_history` is explicitly modeled with `child_job_name` and `child_build_number`, and `storage.set_tracked_in()` only scopes to those columns if they’re provided.

### Fix Focus Areas
- src/rootcoz/main.py[5246-5251]
- src/rootcoz/main.py[5224-5233]
- src/rootcoz/storage.py[2039-2072]
- src/rootcoz/storage.py[513-547]

### Suggested fix
- Update `_add_tracker_comment()` to pass child scope:
 - `child_job_name=body.child_job_name`
 - `child_build_number=body.child_build_number`
- Consider adding a regression test where the same `test_name` appears in multiple child jobs and ensure only the intended row is updated.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. job_names sliced to 1000 ✓ Resolved 📘 Rule violation ≡ Correctness
Description
list_results_for_dashboard_filtered() truncates job_names/exclude_job_names to a fixed
_MAX_IN_CLAUSE via [:_MAX_IN_CLAUSE], which can silently drop matches/exclusions and return
incomplete dashboard data. This is fixed-length slicing in shared storage code without a documented
external contract or configurability for the limit.
Code

src/rootcoz/storage.py[R2911-2940]

+    _MAX_IN_CLAUSE = 1000
+
+    if job_names is not None:
+        if not job_names:
+            # No matching job names from metadata — return empty
+            return {"jobs": [], "total": 0}
+        if len(job_names) > _MAX_IN_CLAUSE:
+            logger.warning(
+                "Dashboard filter: job_names set has %d entries (limit %d). "
+                "Results may be incomplete.",
+                len(job_names),
+                _MAX_IN_CLAUSE,
+            )
+        names_list = sorted(job_names)[:_MAX_IN_CLAUSE]
+        placeholders = ", ".join("?" for _ in names_list)
+        conditions.append(
+            f"json_extract(r.result_json, '$.job_name') IN ({placeholders})"
+        )
+        params.extend(names_list)
+
+    if exclude_job_names:
+        if len(exclude_job_names) > _MAX_IN_CLAUSE:
+            logger.warning(
+                "Dashboard filter: exclude_job_names set has %d entries (limit %d). "
+                "Some exclusions may be skipped.",
+                len(exclude_job_names),
+                _MAX_IN_CLAUSE,
+            )
+        exclude_list = sorted(exclude_job_names)[:_MAX_IN_CLAUSE]
+        placeholders = ", ".join("?" for _ in exclude_list)
Relevance

⭐⭐⭐ High

Team has accepted multiple “no arbitrary truncation/slicing” fixes; will likely change/remove
[:1000] truncation.

PR-#131
PR-#144

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule forbids arbitrary fixed-length slicing truncation in shared code. The new logic
sets _MAX_IN_CLAUSE = 1000 and slices sorted(job_names)/sorted(exclude_job_names) to that
maximum, explicitly warning that results may be incomplete—evidence of truncation.

Rule 804989: Avoid arbitrary data truncation via fixed-length slicing
src/rootcoz/storage.py[2911-2945]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`list_results_for_dashboard_filtered()` truncates `job_names` and `exclude_job_names` using `sorted(... )[:_MAX_IN_CLAUSE]`, which arbitrarily drops data and can make filtered dashboard results incomplete.

## Issue Context
The compliance rule prohibits fixed-length slicing truncation in shared/domain code unless it enforces a documented external contract and is configurable; this code currently truncates in the storage layer.

## Fix Focus Areas
- src/rootcoz/storage.py[2911-2945]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. get_tracked_in_endpoint missing auth check ✓ Resolved 📘 Rule violation ⛨ Security
Description
The new GET /results/{job_id}/tracked-in endpoint is added without any
authentication/authorization enforcement, making a non-public endpoint accessible without
protection. This can expose job-linked tracking URLs to unauthenticated callers.
Code

src/rootcoz/main.py[R5514-5524]

+@app.get("/results/{job_id}/tracked-in")
+async def get_tracked_in_endpoint(
+    job_id: str,
+    _: None = Depends(_bind_job_id),
+) -> dict:
+    """Return tracked-in data for all failures in a job."""
+    result = await get_result(job_id)
+    if not result:
+        raise HTTPException(status_code=404, detail="Job not found")
+    tracked = await storage.get_tracked_in_for_job(job_id)
+    return {"job_id": job_id, "tracked_in": tracked}
Relevance

⭐⭐⭐ High

Auth/allow-list enforcement on endpoints is consistently required and accepted historically (PR
#104) and on new endpoints (PR #133).

PR-#104
PR-#133

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 805015 requires authentication for all non-public API endpoints. The added handler
for GET /results/{job_id}/tracked-in returns data without applying any auth middleware/checks
(e.g., _check_allow_list(request)), so it violates the rule.

Rule 805015: Require authentication for all non-public API endpoints
src/rootcoz/main.py[5514-5524]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /results/{job_id}/tracked-in` is a newly added non-public API endpoint but it does not enforce authentication/authorization.

## Issue Context
Per compliance requirements, all non-public endpoints must require authentication. Other endpoints commonly enforce this via `_check_allow_list(request)` (and sometimes role gates like `_require_reviewer(request)`).

## Fix Focus Areas
- src/rootcoz/main.py[5514-5524]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (7)
4. OFFSET without LIMIT ✓ Resolved 🐞 Bug ≡ Correctness
Description
list_results_for_dashboard() and list_results_for_dashboard_filtered() append OFFSET ? even
when limit=0 (documented as “no limit”), producing invalid SQLite like ... ORDER BY ... OFFSET ?
and causing 500s for /api/dashboard?limit=0&offset=N and
/api/dashboard/filtered?limit=0&offset=N.
Code

src/rootcoz/storage.py[R2769-2776]

+        sql = _DASHBOARD_BASE_SQL + " ORDER BY r.created_at DESC"
+        params: list = []
        if limit > 0:
            sql += " LIMIT ?"
-            params = (limit,)
+            params.append(limit)
+        if offset > 0:
+            sql += " OFFSET ?"
+            params.append(offset)
Relevance

⭐⭐⭐ High

SQL correctness issues are typically fixed; storage query/DB bugfixes commonly accepted (e.g.,
PR102, PR146).

PR-#102
PR-#146

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API documents limit=0 as “no limit” while still accepting offset, but storage builds SQL
with OFFSET regardless of whether a LIMIT clause is present, which can produce invalid SQLite
syntax.

src/rootcoz/main.py[6577-6583]
src/rootcoz/storage.py[2748-2777]
src/rootcoz/storage.py[2880-2888]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`OFFSET` is appended even when `limit=0` (“no limit”), which can generate invalid SQL (`... OFFSET ?` without a `LIMIT`) and break the dashboard endpoints for supported inputs.

### Issue Context
- API explicitly allows `limit=0` (“no limit”) and also exposes `offset`.
- SQLite requires `OFFSET` to be paired with `LIMIT` (or `LIMIT -1`).

### Fix Focus Areas
- src/rootcoz/storage.py[2768-2776]
- src/rootcoz/storage.py[2881-2888]

### Proposed fix
- When `offset > 0` and `limit == 0`, emit `LIMIT -1 OFFSET ?` instead of `OFFSET ?`.
 - Option A: if `limit == 0 and offset > 0`, append `" LIMIT -1"` before appending `" OFFSET ?"`.
 - Apply the same logic in both `list_results_for_dashboard()` and `list_results_for_dashboard_filtered()`.
- (Optional) Add/adjust tests for `/api/dashboard?limit=0&offset=...` and `/api/dashboard/filtered?limit=0&offset=...` to ensure no 500s.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Tracked-in key collision ✓ Resolved 🐞 Bug ≡ Correctness
Description
FailureCard renders tracked-in links keyed only by rep.test_name, collapsing failures that share the
same test name across different child jobs/builds. When duplicates exist, the badge can show the
wrong issue and updates can apply to multiple failure_history rows for the job.
Code

frontend/src/pages/report/FailureCard.tsx[R398-409]

+            {trackedIn[rep.test_name] && trackedIn[rep.test_name].tracked_in_url ? (
+              <TrackedInBadge url={trackedIn[rep.test_name].tracked_in_url} type={trackedIn[rep.test_name].tracked_in_type} />
+            ) : !isViewer ? (
+              <Tooltip>
+                <TooltipTrigger asChild>
+                  <button
+                    onClick={(e) => { e.stopPropagation(); setTrackInOpen(true) }}
+                    className="flex items-center gap-1 rounded-md bg-surface-elevated px-2 py-1 text-[10px] font-mono text-text-tertiary hover:text-text-secondary hover:bg-surface-elevated/80 transition-colors"
+                  >
+                    <Link2 className="h-3 w-3" />
+                    Track
+                  </button>
Relevance

⭐⭐⭐ High

Team previously fixed cross-child collisions by adding child scoping/joins; similar issues accepted
in PRs #133, #146, #59.

PR-#133
PR-#146
PR-#59

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new UI reads tracked-in solely by rep.test_name. However, failure_history explicitly stores
child_job_name/child_build_number and indexes them with job_id,test_name, indicating duplicate
test_name rows per job are expected. The storage layer’s tracked-in update and retrieval are also
keyed only by job_id,test_name, so duplicates will be overwritten on read and may be multi-updated
on write.

frontend/src/pages/report/FailureCard.tsx[398-413]
src/rootcoz/storage.py[512-546]
src/rootcoz/storage.py[2013-2037]
PR-#146

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`FailureCard` looks up tracked-in data with `trackedIn[rep.test_name]`. This assumes `test_name` is unique within a job, but the data model includes `child_job_name`/`child_build_number`, so multiple failures in a single job can share the same `test_name`.

### Issue Context
Backend storage updates tracked-in with `WHERE job_id = ? AND test_name = ?` and returns a dict keyed by `test_name`, which means duplicates will either be overwritten (GET) or multi-updated (PUT). The frontend already has an established way to disambiguate failures via `reviewKey(child_job_name, child_build_number, test_name)`.

### Fix Focus Areas
- frontend/src/pages/report/FailureCard.tsx[398-413]
- frontend/src/lib/reviewKey.ts[1-5]
- src/rootcoz/storage.py[512-546]
- src/rootcoz/storage.py[2013-2037]
- src/rootcoz/storage.py[2040-2061]

### Suggested fix approach
1. Change tracked-in data shape to be keyed by the same compound key used elsewhere (e.g., `reviewKey(child_job_name, child_build_number, test_name)`), or return a list of entries including child context.
2. Update the backend `set_tracked_in` path to uniquely target a single failure_history row by including `child_job_name` and `child_build_number` in the WHERE clause (and accept them in the request body).
3. Update the frontend `FailureCard` to read tracked-in using the compound key (or match the returned list entry for the current rep’s child context).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Tracked-in URL XSS ✓ Resolved 🐞 Bug ⛨ Security
Description
Tracked-in URLs are accepted with only whitespace trimming and rendered directly into an <a href>,
allowing dangerous schemes like javascript: to be stored and executed when clicked. This creates a
stored XSS vector across authenticated users viewing job results.
Code

frontend/src/pages/report/TrackedInBadge.tsx[R46-50]

+        <a
+          href={url}
+          target="_blank"
+          rel="noopener noreferrer"
+          className="inline-flex items-center gap-1 rounded-md bg-accent-blue/10 px-2 py-0.5 text-[10px] font-medium text-accent-blue hover:bg-accent-blue/20 transition-colors"
Relevance

⭐⭐⭐ High

Team previously accepted URL/href safety checks (isSafeHref usage) to prevent unsafe links in PR
#122.

PR-#122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The backend does not validate URL schemes (only strips), and the frontend uses the stored value
directly as an anchor href, enabling dangerous-scheme injection.

src/rootcoz/models.py[854-876]
frontend/src/pages/report/TrackedInBadge.tsx[41-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
User-provided tracked-in URLs are persisted and later rendered as `<a href={url}>` without validating the URL scheme. A malicious value like `javascript:...` (or `data:`) can be stored and will execute when clicked.

### Issue Context
- Backend model validator for `url` only strips whitespace.
- Frontend renders `href={url}`.

### Fix Focus Areas
- src/rootcoz/models.py[872-875]
- frontend/src/pages/report/TrackedInBadge.tsx[46-58]

### Implementation notes
- Backend: enforce an allowlist of schemes (at minimum `http` and `https`) and reject anything else with 400. Consider using Pydantic’s `HttpUrl`/`AnyHttpUrl` or a custom validator using `urllib.parse`.
- Optionally: if `type` is `github`/`jira`, restrict hostnames accordingly or require `https://`.
- Frontend: defensively refuse to render as a link if `new URL(url).protocol` is not `http:`/`https:` (render as plain text + warning).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. SSRF in URL autodetect ✓ Resolved 🐞 Bug ⛨ Security
Description
_detect_tracker_type_via_http() runs a curl -L HEAD request to a user-supplied URL, enabling SSRF to
internal services (including via redirects) and making it easy to tie up server capacity by spawning
many slow subprocesses. This is reachable from PUT /results/{job_id}/tracked-in when the caller
omits type and the URL doesn’t match simple github/jira substrings.
Code

src/rootcoz/main.py[R5446-5456]

+    try:
+        proc = await asyncio.create_subprocess_exec(
+            "curl",
+            "-sI",
+            "-L",
+            "--max-time",
+            "5",
+            url,
+            stdout=asyncio.subprocess.PIPE,
+            stderr=asyncio.subprocess.PIPE,
+        )
Relevance

⭐⭐ Medium

No direct SSRF precedent found; team often accepts security hardening (e.g., remove curl|bash,
enforce user creds).

PR-#140
PR-#132
PR-#104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper function executes curl -sI -L --max-time 5 <url> which performs an outbound request and
follows redirects, and the endpoint calls it when type cannot be inferred locally. The request model
only validates scheme, so arbitrary internal/redirecting URLs are permitted to reach the
autodetection path.

src/rootcoz/main.py[5440-5463]
src/rootcoz/main.py[5486-5495]
src/rootcoz/models.py[854-884]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PUT /results/{job_id}/tracked-in` can trigger `_detect_tracker_type_via_http(body.url)`, which shells out to `curl` and follows redirects (`-L`) to a user-provided URL. This introduces an SSRF vector (internal host access, metadata endpoints, redirect chains) and a DoS risk (subprocess per request, multi-second timeouts).

### Issue Context
- The request body validation enforces only `http/https` scheme and does not restrict host/IP.
- Autodetection is best-effort; returning an empty type is already handled by callers/UI.

### Fix Focus Areas
- src/rootcoz/main.py[5440-5465]

### Suggested fix approach
- Preferred: remove `_detect_tracker_type_via_http()` entirely and only detect type via deterministic string heuristics; otherwise leave `tracked_type` empty when unknown.
- If HTTP-based detection is required:
 - use an in-process HTTP client (e.g., `httpx`) with strict timeouts,
 - disable redirects (or restrict redirects to same registrable domain),
 - block private/loopback/link-local IP ranges after DNS resolution,
 - optionally restrict to an explicit allowlist of Jira hostnames from settings,
 - add rate limiting/caching to reduce abuse potential.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Debounce ref readonly type ✓ Resolved 🐞 Bug ≡ Correctness
Description
DashboardPage uses useRef<ReturnType<typeof setTimeout>>(null) and then assigns to .current; with
React’s typings this selects a readonly RefObject overload and fails TypeScript compilation under
strict mode. This is a build-blocking frontend regression.
Code

frontend/src/pages/DashboardPage.tsx[R121-127]

+  const searchDebounceRef = useRef<ReturnType<typeof setTimeout>>(null)
+
+  // Debounce search input (300ms) to avoid excessive backend requests
+  useEffect(() => {
+    if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current)
+    searchDebounceRef.current = setTimeout(() => setDebouncedSearch(search), 300)
+    return () => { if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current) }
Relevance

⭐⭐ Medium

Mixed history on TS strict-type fixes (one rejected in PR #102; similar TS-break note in PR #87
undetermined).

PR-#102
PR-#87

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ref is initialized with null and later assigned, and the frontend is configured for strict
TypeScript checking, making this a compile-time error with React’s ref overloads.

frontend/src/pages/DashboardPage.tsx[112-128]
frontend/tsconfig.app.json[19-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useRef<ReturnType<typeof setTimeout>>(null)` triggers React’s overload that returns a readonly `RefObject`, so `searchDebounceRef.current = ...` is a TypeScript error in strict mode.

### Issue Context
Frontend TS config has `strict: true`.

### Fix Focus Areas
- frontend/src/pages/DashboardPage.tsx[121-127]

### Implementation notes
- Change to `useRef<ReturnType<typeof setTimeout> | null>(null)` (mutable ref) or `useRef<number | null>(null)` in browser builds.
- Ensure all `clearTimeout(...)` calls handle the nullable case safely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Timeout status filter broken ✓ Resolved 🐞 Bug ≡ Correctness
Description
The dashboard sends selected statuses to the backend, but the UI includes a derived 'timeout' status
that is computed from failed jobs’ error/summary. Because SQL filters only on results.status,
selecting 'timeout' will return no matches.
Code

src/rootcoz/storage.py[R2825-2828]

+    if status:
+        placeholders = ", ".join("?" for _ in status)
+        conditions.append(f"r.status IN ({placeholders})")
+        params.extend(status)
Relevance

⭐⭐ Medium

No historical evidence found for backend supporting derived “timeout” status filtering semantics.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The UI offers 'timeout' and derives it from failed jobs, but the backend only filters by stored
statuses in the results table, so 'timeout' can never match.

frontend/src/pages/DashboardPage.tsx[48-48]
frontend/src/lib/utils.ts[18-39]
src/rootcoz/storage.py[2782-2829]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Dashboard status filtering was moved into SQL (`r.status IN (...)`), but the frontend exposes a `timeout` option that is not a stored status value; it’s derived when `status === 'failed'` and error/summary matches timeout patterns. This makes the `timeout` filter silently return zero results.

### Issue Context
- Frontend status options include `timeout`.
- Frontend’s `isAnalysisTimeout()` shows the exact derivation rules.

### Fix Focus Areas
- src/rootcoz/storage.py[2825-2828]

### Implementation notes
- Option A (recommended): when `status` includes `timeout`, translate it into a SQL predicate equivalent to `isAnalysisTimeout` (e.g., `r.status='failed' AND (lower(r.error) LIKE ... OR lower(json_extract(r.result_json,'$.summary')) LIKE ...)`).
- Option B: support a separate query param like `timeout=1` and keep `status=failed`.
- Option C: remove `timeout` from server-side status filtering and keep it client-side (but that reintroduces truncation pitfalls unless handled carefully).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Date_to filter excludes day ✓ Resolved 🐞 Bug ≡ Correctness
Description
list_results_for_dashboard_filtered() compares results.created_at timestamps directly against
date-only strings, so date_to='YYYY-MM-DD' will exclude all rows later that same day. This breaks
dashboard date range filtering and makes valid older jobs disappear from filtered results.
Code

src/rootcoz/storage.py[R2830-2836]

+    if date_from:
+        conditions.append("r.created_at >= ?")
+        params.append(date_from)
+
+    if date_to:
+        conditions.append("r.created_at <= ?")
+        params.append(date_to)
Relevance

⭐⭐ Medium

Similar date-bound correctness change was previously rejected (PR #103), so acceptance for date-only
end-of-day fix is unclear.

PR-#103

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The frontend emits date-only values and sends them unchanged, while the backend compares them
against timestamp strings; the repo already contains a helper that uses date(column) for this
exact date-only input pattern.

frontend/src/components/shared/DateRangePresetFilter.tsx[21-36]
frontend/src/components/shared/DateRangePresetFilter.tsx[76-84]
frontend/src/pages/DashboardPage.tsx[287-304]
src/rootcoz/storage.py[2782-2836]
src/rootcoz/storage.py[5199-5215]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Dashboard filtering accepts `date_from`/`date_to` as `YYYY-MM-DD`, but the SQL currently uses `r.created_at >= ?` / `<= ?` against timestamp strings. This causes `date_to` to exclude same-day rows (because `2026-07-01T12:00:00` > `2026-07-01`).

### Issue Context
Frontend `DateRangePresetFilter` emits date-only strings, and `DashboardPage` forwards them unchanged to `/api/dashboard/filtered`.

### Fix Focus Areas
- src/rootcoz/storage.py[2830-2836]

### Implementation notes
- Prefer reusing the existing helper pattern used by reports: `date(r.created_at) >= ?` and `date(r.created_at) <= ?`.
- Alternatively, convert bounds to full timestamps (start-of-day / end-of-day) before comparing against `created_at`.
- Update docstrings/comments to match the implemented semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

11. Tracked-in update not verified ✓ Resolved 🐞 Bug ☼ Reliability
Description
_add_tracker_comment() ignores the rowcount returned by storage.set_tracked_in(), so a mismatch
(wrong test_name/child scope) can silently leave tracked-in unset after successful issue creation.
This makes the new auto-linking behavior unreliable and hard to debug.
Code

src/rootcoz/main.py[R5250-5255]

+                job_id,
+                body.test_name,
+                issue_url,
+                tracked_type,
+                child_job_name=body.child_job_name,
+                child_build_number=body.child_build_number,
Relevance

⭐⭐⭐ High

Team often accepts reliability hardening to prevent silent failures (similar patterns accepted in
PRs 122, 151).

PR-#122
PR-#151

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The storage function explicitly returns cursor.rowcount and does not raise when 0 rows are updated;
the new call site does not check that value, so 0-updates are silent unless an exception occurs.

src/rootcoz/storage.py[2039-2075]
src/rootcoz/main.py[5246-5262]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`storage.set_tracked_in()` returns the number of rows updated. `_add_tracker_comment()` awaits the call but does not inspect this value, and only logs on exceptions. If 0 rows are updated (e.g., due to mismatched child scoping), the system will appear to have succeeded (issue created, comment added) but tracked-in will not actually be persisted.

## Issue Context
This is specifically in the automatic post-issue-creation path, which is meant to be best-effort; even so, a 0-row update is a meaningful signal that should be logged (or otherwise surfaced).

## Fix Focus Areas
- src/rootcoz/main.py[5250-5255]

## Suggested implementation direction
- Capture the return value:
 - `updated = await storage.set_tracked_in(...)`
- If `updated == 0`, emit a warning with job_id, test_name, and child scope (but do not include secrets).
- (Optional) consider incrementing a metric or adding structured logging fields so this is searchable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. CLI type option misleading ✓ Resolved 🐞 Bug ≡ Correctness
Description
The CLI advertises "--type … auto-detect", but SetTrackedInRequest only accepts "jira", "github", or
empty; passing "auto-detect" will reliably fail with a 422 validation error.
This breaks the documented CLI behavior for setting tracked-in.
Code

src/rootcoz/cli/main.py[R670-672]

+    tracked_type: str = typer.Option(
+        "", "--type", help="Tracker type: jira, github, or auto-detect."
+    ),
Relevance

⭐⭐⭐ High

Team previously accepted fixes preventing CLI/server option drift (valid roles reuse) in PR #104.

PR-#104

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI explicitly suggests auto-detect as a valid --type value, but backend validation rejects
any non-empty value not in (jira, github). Backend auto-detection is triggered only when type is
empty, so the CLI-documented value will always error.

src/rootcoz/cli/main.py[663-672]
src/rootcoz/models.py[854-870]
src/rootcoz/main.py[5478-5482]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`rootcoz results set-tracked-in --type` help text suggests an `auto-detect` value, but the backend request model rejects any non-empty value other than `jira`/`github`. Backend auto-detection only happens when the type is omitted/empty.

### Issue Context
- CLI passes the provided `tracked_type` through to the API.
- Backend validates `type` and only auto-detects when it is empty.

### Fix Focus Areas
- src/rootcoz/cli/main.py[670-672]
- src/rootcoz/models.py[854-870]
- src/rootcoz/main.py[5478-5482]

### Suggested fix
- Either:
 1) Change CLI help text to: “Tracker type: jira, github, or omit to auto-detect.”
 2) Or accept `auto`/`auto-detect` in the CLI and translate it to `""` before calling the client.
- Optionally enforce choices via Typer (e.g., a Literal/Enum) to prevent invalid values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Invalid review_status accepted ✓ Resolved 🐞 Bug ≡ Correctness
Description
/api/dashboard/filtered accepts any review_status string and passes it into
list_results_for_dashboard_filtered(), but the storage layer only applies filtering for
'reviewed' and 'not_reviewed'. Invalid values are silently treated as “all”, producing
incorrect/unfiltered results without an error.
Code

src/rootcoz/main.py[R8250-8252]

+    review_status: str = Query(
+        default="all", description="Filter: all, reviewed, not_reviewed"
+    ),
Relevance

⭐⭐⭐ High

PR #144 added _validate_review_status to reject invalid values (no silent fallback); pattern
suggests they'd accept this fix.

PR-#144

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The endpoint forwards review_status directly to storage, while storage only recognizes two
specific values; therefore any typo/unknown value will be silently ignored and behave like “all”.

src/rootcoz/main.py[8243-8314]
src/rootcoz/storage.py[2912-2922]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The dashboard filtered endpoint passes `review_status` through without validation, and unknown values degrade silently into an unfiltered query. This masks client/CLI bugs and makes it hard to detect incorrect requests.

### Issue Context
- API accepts `review_status: str` with default `"all"`.
- Storage only branches for `"reviewed"` and `"not_reviewed"`; anything else is ignored.

### Fix Focus Areas
- src/rootcoz/main.py[8243-8302]
- src/rootcoz/storage.py[2912-2922]

### Suggested fix
- Validate `review_status` at the API boundary (FastAPI): allow only `{all, reviewed, not_reviewed}` and return HTTP 400/422 for invalid values.
- (Optional hardening) Add a defensive check in `list_results_for_dashboard_filtered()` to reject unknown values if the API layer ever regresses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (11)
14. Invalid child scope sent ✓ Resolved 🐞 Bug ☼ Reliability
Description
The CLI client includes child_build_number in the request body even when child_job_name is
empty, but the backend model rejects child_build_number > 0 without child_job_name, causing the
command to fail with a validation error. This is avoidable by validating in the CLI and only sending
child_build_number when child_job_name is present.
Code

src/rootcoz/cli/client.py[R311-315]

+        body: dict = {"test_name": test_name, "url": url, "type": tracked_type}
+        if child_job_name:
+            body["child_job_name"] = child_job_name
+        if child_build_number:
+            body["child_build_number"] = child_build_number
Relevance

⭐⭐⭐ High

Repo history shows strong preference for preventing invalid child scoping combos; likely accept
CLI-side guard.

PR-#133
PR-#162

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI client serializes child_build_number independently of child_job_name, but the backend
model validator requires child_job_name whenever child_build_number > 0, so the request will be
rejected when users pass --child-build without --child-job.

src/rootcoz/cli/client.py[300-320]
src/rootcoz/cli/main.py[657-681]
src/rootcoz/models.py[656-672]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Client.set_tracked_in()` will send `child_build_number` whenever it is non-zero, even if `child_job_name` is not set. The backend request validator explicitly rejects this combination, so the CLI can easily produce a 422 response.

## Issue Context
The backend uses `_ChildJobFieldsValidator` which requires `child_job_name` when `child_build_number > 0`. The CLI should mirror this validation and/or avoid emitting invalid payloads.

## Fix Focus Areas
- src/rootcoz/cli/client.py[311-315]
- src/rootcoz/cli/main.py[657-681]
- src/rootcoz/models.py[656-672]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Stale totals after delete ✓ Resolved 🐞 Bug ≡ Correctness
Description
DashboardPage now drives pagination and the summary count from totalJobs, but
handleDelete/handleBulkDelete only update jobs and never adjust totalJobs. After deletions this can
show incorrect totals/totalPages and leave the user on empty pages until a refetch occurs.
Code

frontend/src/pages/DashboardPage.tsx[R414-417]

+  // Server-side pagination: sorted is already the current page
+  const totalPages = Math.max(1, Math.ceil(totalJobs / perPage))
  const safePage = Math.min(page, totalPages)
-  const pageJobs = sorted.slice((safePage - 1) * perPage, safePage * perPage)
+  const pageJobs = sorted
Relevance

⭐⭐⭐ High

Team previously accepted fixes preventing empty/out-of-range pagination pages (PRs #103, #109); same
stale-page issue post-delete.

PR-#103
PR-#109

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The UI uses totalJobs for pagination and display, but delete handlers only remove entries from the
current page’s jobs array; nothing updates totalJobs, so counts diverge until a refetch happens.

frontend/src/pages/DashboardPage.tsx[112-131]
frontend/src/pages/DashboardPage.tsx[414-418]
frontend/src/pages/DashboardPage.tsx[476-519]
frontend/src/pages/DashboardPage.tsx[627-631]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dashboard pagination and the summary count are now derived from `totalJobs`, but deletion flows only mutate `jobs`. This leaves `totalJobs` stale after single/bulk deletes, causing incorrect run counts and page counts.

## Issue Context
- `totalPages` is computed from `totalJobs / perPage`, and the summary row displays `totalJobs`.
- `handleDelete()` and `handleBulkDelete()` only call `setJobs(...)` and do not call `setTotalJobs(...)`.

## Fix Focus Areas
- frontend/src/pages/DashboardPage.tsx[414-418]
- frontend/src/pages/DashboardPage.tsx[476-519]
- frontend/src/pages/DashboardPage.tsx[627-631]

## Suggested fix
- In `handleDelete()`: after a successful delete, call `setTotalJobs(prev => Math.max(0, prev - 1))`.
- In `handleBulkDelete()`: after receiving `data.deleted`, call `setTotalJobs(prev => Math.max(0, prev - data.deleted.length))`.
- Optional: if you want perfect consistency, trigger a `fetchJobsRef.current()` after deletion to refresh `{jobs,total}` from the server (or do it conditionally when page becomes empty).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Duplicated tracker-type URL detection ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Tracker type detection logic is duplicated across multiple frontend modules, increasing the risk of
drift and inconsistent behavior. This violates the requirement to extract shared TypeScript logic
into lib/ utilities when reused.
Code

frontend/src/pages/report/FailureCard.tsx[R748-751]

+          onIssueCreated={(url) => {
+            // Auto-set tracked-in in local state (backend already persisted it)
+            const trackedType = url.includes('github.com') ? 'github' : url.includes('jira') || url.includes('atlassian') ? 'jira' : ''
+            dispatch({ type: 'SET_TRACKED_IN_ENTRY', payload: { testName: rep.test_name, entry: { tracked_in_url: url, tracked_in_type: trackedType } } })
Relevance

⭐⭐⭐ High

Repo enforces dedup/shared TS logic; similar duplication extractions were accepted (PRs #132, #133).

PR-#132
PR-#133

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 804996 requires shared TypeScript logic used across modules to be extracted into
lib/ utilities. The URL→tracker-type detection is duplicated in FailureCard.tsx and
TrackedInBadge.tsx instead of being implemented once and imported.

Rule 804996: Extract shared TypeScript logic into lib/ utilities
frontend/src/pages/report/FailureCard.tsx[748-751]
frontend/src/pages/report/TrackedInBadge.tsx[88-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Tracker type detection from a URL is implemented in more than one frontend module (duplicate logic), which should be centralized into a reusable utility under `lib/`.

## Issue Context
This logic is currently duplicated in `FailureCard` and `TrackedInBadge`/`TrackInDialog`. Centralizing it will avoid inconsistent detection rules and reduce maintenance overhead.

## Fix Focus Areas
- frontend/src/pages/report/FailureCard.tsx[748-751]
- frontend/src/pages/report/TrackedInBadge.tsx[88-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Review filter mispaginated ✓ Resolved 🐞 Bug ≡ Correctness
Description
Dashboard reviewStatus filtering is now applied client-side after fetching a single server-side
page, while totalJobs/totalPages come from the backend total; this can produce empty pages and
totals/counts that do not correspond to the displayed reviewed/not_reviewed rows.
Code

frontend/src/pages/DashboardPage.tsx[R354-362]

+  // Client-side review status filter (not pushed to backend)
  const filtered = useMemo(() => {
-    const fromBound = dateFrom ? utcStartOfDateInput(dateFrom) : null
-    const toBound = dateTo ? utcEndOfDateInput(dateTo) : null
-
+    if (reviewStatus === 'all') return jobs
    return jobs.filter((j) => {
-      if (selectedStatuses.size > 0) {
-        const displayStatus = isAnalysisTimeout(j.status, j.error, j.summary) ? 'timeout' : j.status
-        if (!selectedStatuses.has(displayStatus)) return false
-      }
-
-      if (fromBound || toBound) {
-        const jobDate = parseApiTimestamp(j.created_at)
-        if (Number.isNaN(jobDate.getTime())) return false
-        if (fromBound && jobDate < fromBound) return false
-        if (toBound && jobDate > toBound) return false
-      }
-
-      if (reviewStatus === 'reviewed' && j.reviewed_count === 0) return false
-      if (reviewStatus === 'not_reviewed' && j.reviewed_count > 0) return false
-
-      if (!search) return true
-      const q = search.toLowerCase()
-      return (j.job_name ?? '').toLowerCase().includes(q) || j.job_id.toLowerCase().includes(q)
+      if (reviewStatus === 'reviewed') return j.reviewed_count > 0
+      if (reviewStatus === 'not_reviewed') return j.reviewed_count === 0
+      return true
    })
-  }, [jobs, search, selectedStatuses, reviewStatus, dateFrom, dateTo])
+  }, [jobs, reviewStatus])
Relevance

⭐⭐⭐ High

Team fixes pagination/filter mismatches; accepted empty-page pagination bugs before (PR103).

PR-#103

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The frontend fetches a paginated page and total from the backend without including reviewStatus,
then filters the page locally by reviewStatus while still using backend totalJobs to compute
totalPages and display totals.

frontend/src/pages/DashboardPage.tsx[287-311]
frontend/src/pages/DashboardPage.tsx[354-362]
frontend/src/pages/DashboardPage.tsx[414-417]
frontend/src/pages/DashboardPage.tsx[627-631]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
With server-side pagination (`limit`/`offset`), leaving `reviewStatus` as a client-side filter means filtering happens *after* pagination and after `totalJobs` is computed. This breaks pagination semantics (pages can be empty) and makes the summary/total pages misleading.

### Issue Context
- `fetchJobs()` requests one page and sets `totalJobs` from backend `total`.
- `reviewStatus` then filters the returned page locally.

### Fix Focus Areas
- frontend/src/pages/DashboardPage.tsx[287-311]
- frontend/src/pages/DashboardPage.tsx[354-362]
- frontend/src/pages/DashboardPage.tsx[414-417]

### Proposed fix
- Add a backend filter for review status to `/api/dashboard/filtered` (e.g. `review_status=all|reviewed|not_reviewed`) and include it in:
 - the `COUNT(*)` query (for `total`)
 - the paginated fetch query.
 Use an `EXISTS`/`NOT EXISTS` predicate on `failure_reviews` (reviewed=1) to avoid relying on the `reviewed_count` SELECT alias in `WHERE`.
- Update the frontend to send this new `review_status` param and remove the client-side filter (or only use it as a display-only toggle without affecting pagination/totals).
- Ensure the summary row uses the same total that matches what is actually displayed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. Tracked-in URL logged unredacted ✓ Resolved 📘 Rule violation ⛨ Security

<br...

Comment thread src/rootcoz/storage.py Outdated
Comment thread src/rootcoz/storage.py Outdated
Comment thread frontend/src/pages/DashboardPage.tsx Outdated
Comment thread frontend/src/pages/report/TrackedInBadge.tsx Outdated
Comment thread src/rootcoz/main.py Outdated
Comment thread src/rootcoz/storage.py Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d2d638d

Comment thread src/rootcoz/main.py Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8e43a95

@rnetser

rnetser commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator
image

the tracked ref button should be next to the classification, left to "AI for issue generation", it is not visible at the top bar

Comment thread frontend/src/pages/report/FailureCard.tsx Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 47d4ebf

@myakove

myakove commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Fixed — moved the "Track" button to the failure card header row, positioned right after the classification/pattern badges. It's now visible without expanding the card.

Changes:

  • Track button (for unlinked failures) now appears in the top bar next to classification, as a compact badge-style button
  • TrackedIn badge (when a URL is already set) also shows in the same position
  • Removed the old "Track In..." button from the expanded action bar (it was hidden and required expanding the card)

Pushed to the branch and deployed to dev. Please re-test.

Comment thread src/rootcoz/main.py Outdated
Comment thread src/rootcoz/storage.py
Comment thread frontend/src/pages/ReportPage.tsx Outdated
Comment thread frontend/src/pages/DashboardPage.tsx Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0e00e84

@myakove myakove force-pushed the feat/issue-155-156-tracked-in-dashboard branch from 602af19 to 6353282 Compare July 5, 2026 08:48
Comment thread src/rootcoz/main.py
Comment thread src/rootcoz/cli/main.py
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6353282

Comment thread src/rootcoz/main.py
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9d159cd

Comment thread src/rootcoz/main.py
Comment on lines +5249 to +5265
updated = await storage.set_tracked_in(
job_id,
body.test_name,
issue_url,
tracked_type,
child_job_name=body.child_job_name,
child_build_number=body.child_build_number,
)
if updated == 0:
logger.warning(
"Tracked-in auto-set matched 0 rows for "
"job_id=%s, test_name=%s, child=%s/%s",
job_id,
body.test_name,
body.child_job_name,
body.child_build_number,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Unreported multi-row update 🐞 Bug ◔ Observability

In _add_tracker_comment(), auto-setting tracked-in only logs when storage.set_tracked_in()
updates 0 rows; if it updates >1 rows, the code silently proceeds, making it hard to detect
unexpected broad updates. This can mask mismatched scoping (e.g., wildcard child_build_number=0 or
duplicate failure_history rows) and complicate debugging when tracked-in links appear on multiple
failures.
Agent Prompt
## Issue description
`_add_tracker_comment()` auto-sets tracked-in via `storage.set_tracked_in()` and only warns when `updated == 0`. When `updated > 1`, there is no log/telemetry indicating a broad write occurred, which can hide incorrect scoping and make support/debugging difficult.

## Issue Context
- `storage.set_tracked_in()` can legitimately update multiple rows depending on inputs (e.g., wildcard semantics when `child_build_number == 0`, or multiple matching rows).
- The caller currently treats any `updated > 0` as success without surfacing the breadth of the update.

## Fix Focus Areas
- src/rootcoz/main.py[5249-5265]

### Suggested fix
- Change the rowcount check to:
  - warn when `updated == 0` (existing behavior)
  - additionally warn when `updated > 1` (or more generally when `updated != 1`) including job_id, test_name, child scope, and the rowcount.
- Keep the tracker URL out of logs (or log only host) to avoid future leakage risk.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit bd9ab4a

@rnetser

rnetser commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Follow-up: Reorganize FailureCard expanded actions layout

Split the expanded actions bar into a structured multi-line layout, using the existing styling (badges, buttons, inputs, and TrackedInBadge component):

Line Content
ACTIONS AI for issue generation: [provider] [model] · ↻ Re-analyze · GitHub Issue · Jira Ticket · ☐ Include links
CLASSIFY Classification ▾ · Pattern ▾ · 🔗 Track
(indented) Tracked item links, each on its own line (only shown when tracked items exist)

Use the same component styling already in FailureCard.tsx and TrackedInBadge.tsx — no new design tokens or components needed, just a layout reorganization.

Screenshot From 2026-07-05 13-12-23

Comment on lines +398 to 403
{trackedIn[repKey]?.tracked_in_url && (
<TrackedInBadge url={trackedIn[repKey].tracked_in_url} type={trackedIn[repKey].tracked_in_type} />
)}
{rep.reanalyzed_with && rep.reanalysis_status !== 'running' && (
<Tooltip>
<TooltipTrigger asChild>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

1. Duplicate tracked-in badge 🐞 Bug ⚙ Maintainability

FailureCard renders TrackedInBadge in the header and again in the expanded section under the same
condition (trackedIn[repKey]?.tracked_in_url), so expanded cards show the same tracked-in link
twice.
Agent Prompt
### Issue description
`FailureCard` conditionally renders `TrackedInBadge` twice (once in the header badges row and once again in the expanded body). When a failure has `tracked_in_url` and the card is expanded, the same tracked-in link appears twice.

### Issue Context
The duplication is caused by two separate render blocks guarded by the same condition `trackedIn[repKey]?.tracked_in_url`.

### Fix Focus Areas
- frontend/src/pages/report/FailureCard.tsx[398-400]
- frontend/src/pages/report/FailureCard.tsx[725-730]

### Suggested fix
Make the renders mutually exclusive (e.g., show it in the header only when collapsed, or remove the expanded-body copy), so the tracked-in badge/link appears exactly once per card state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4e5d19c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants