fix(dashboard): fix empty widgets + NULL created_at on campaign tests
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

1. metrics_query_service: use NULLS LAST in get_recent_tests() so tests
   with actual dates always appear before NULL-dated ones.

2. campaign_service: set created_at=datetime.utcnow() when creating tests
   from campaigns (was missing, leaving 21 tests with NULL created_at).
   Fixed existing NULL values directly in production DB.

3. DashboardPage: add isError handling to all V2 metric widgets
   (pipeline, team activity, validation rate, recent tests).
   - Add retry:2 to all secondary metric queries so transient failures
     are retried before showing empty state.
   - Show 'Could not load X — refresh' instead of empty/misleading
     'No tests created yet' when a query actually fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-02 08:58:04 +02:00
parent a566834e08
commit 0d4c105aa3
3 changed files with 20 additions and 6 deletions

View File

@@ -181,6 +181,7 @@ def generate_campaign_from_threat_actor(
tool_used=template.tool_suggested,
created_by=user.id,
state=TestState.draft,
created_at=datetime.utcnow(),
)
db.add(test)
db.flush() # Get test.id

View File

@@ -232,10 +232,11 @@ def get_validation_rate(db: Session) -> list[ValidationRate]:
def get_recent_tests(db: Session, *, limit: int = 10) -> list[RecentTestItem]:
"""Return the most recently created tests."""
from sqlalchemy import nullslast
tests = (
db.query(Test)
.options(joinedload(Test.technique))
.order_by(Test.created_at.desc())
.order_by(nullslast(Test.created_at.desc()))
.limit(limit)
.all()
)

View File

@@ -87,25 +87,29 @@ export default function DashboardPage() {
queryFn: getCoverageByTactic,
});
// V2 queries
const { data: pipeline, isLoading: pipelineLoading } = useQuery({
// V2 queries — retry:2 so transient failures don't leave widgets blank
const { data: pipeline, isLoading: pipelineLoading, isError: pipelineError } = useQuery({
queryKey: ["metrics", "test-pipeline"],
queryFn: getTestPipeline,
retry: 2,
});
const { data: teamActivity, isLoading: teamLoading } = useQuery({
const { data: teamActivity, isLoading: teamLoading, isError: teamError } = useQuery({
queryKey: ["metrics", "team-activity"],
queryFn: getTeamActivity,
retry: 2,
});
const { data: validationRates, isLoading: validationLoading } = useQuery({
const { data: validationRates, isLoading: validationLoading, isError: validationError } = useQuery({
queryKey: ["metrics", "validation-rate"],
queryFn: getValidationRate,
retry: 2,
});
const { data: recentTests, isLoading: recentLoading } = useQuery({
const { data: recentTests, isLoading: recentLoading, isError: recentError } = useQuery({
queryKey: ["metrics", "recent-tests"],
queryFn: getRecentTests,
retry: 2,
});
const { data: coverageEvolution, isLoading: evolutionLoading } = useQuery({
@@ -287,6 +291,8 @@ export default function DashboardPage() {
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : pipelineError ? (
<p className="py-8 text-center text-sm text-gray-500">Could not load pipeline data.</p>
) : pipeline ? (
<PipelineFunnel pipeline={pipeline} />
) : null}
@@ -305,6 +311,8 @@ export default function DashboardPage() {
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : teamError ? (
<p className="py-8 text-center text-sm text-gray-500">Could not load team activity.</p>
) : teamActivity ? (
<div className="space-y-4">
{teamActivity.map((team: TeamActivityItem) => {
@@ -355,6 +363,8 @@ export default function DashboardPage() {
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : validationError ? (
<p className="py-8 text-center text-sm text-gray-500">Could not load validation data.</p>
) : validationRates ? (
<div className="space-y-4">
{validationRates.map((rate: ValidationRateItem) => {
@@ -419,6 +429,8 @@ export default function DashboardPage() {
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : recentError ? (
<p className="py-8 text-center text-sm text-gray-500">Could not load recent tests refresh the page.</p>
) : recentTests && recentTests.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">