- Create action_dropdown macro in dropdowns.html supporting: - Loading/disabled state via loading_var parameter - Custom loading label - Icon support - Primary/secondary variants - Update code quality dashboard to use new macro - Add Dropdowns section to components page with examples: - Basic dropdown - Action dropdown with loading state - Context menu (3-dot) - Variant showcase (primary, secondary, ghost) Architecture validation now passes with 0 errors and 0 warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
350 lines
19 KiB
HTML
350 lines
19 KiB
HTML
{# app/templates/admin/code-quality-dashboard.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert_dynamic %}
|
|
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
|
{% from 'shared/macros/dropdowns.html' import action_dropdown, dropdown_item %}
|
|
|
|
{% block title %}Code Quality Dashboard{% endblock %}
|
|
|
|
{% block alpine_data %}codeQualityDashboard(){% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="/static/admin/js/code-quality-dashboard.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% call page_header_flex(title='Code Quality Dashboard', subtitle='Unified code quality tracking: architecture, security, and performance') %}
|
|
{{ refresh_button(variant='secondary') }}
|
|
{% call action_dropdown(
|
|
label='Run Scan',
|
|
loading_label='Scanning...',
|
|
open_var='scanDropdownOpen',
|
|
loading_var='scanning',
|
|
icon='search'
|
|
) %}
|
|
{{ dropdown_item('Run All Validators', 'runScan("all"); scanDropdownOpen = false') }}
|
|
{{ dropdown_item('Architecture Only', 'runScan("architecture"); scanDropdownOpen = false') }}
|
|
{{ dropdown_item('Security Only', 'runScan("security"); scanDropdownOpen = false') }}
|
|
{{ dropdown_item('Performance Only', 'runScan("performance"); scanDropdownOpen = false') }}
|
|
{% endcall %}
|
|
{% endcall %}
|
|
|
|
{{ loading_state('Loading dashboard...') }}
|
|
|
|
{{ error_state('Error loading dashboard') }}
|
|
|
|
{{ alert_dynamic(type='success', message_var='successMessage', show_condition='successMessage') }}
|
|
|
|
<!-- Scan Progress Alert -->
|
|
<div x-show="scanning && scanProgress"
|
|
x-transition
|
|
class="flex items-center p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
|
|
role="alert">
|
|
<span x-html="$icon('spinner', 'w-5 h-5 mr-3 text-blue-500')"></span>
|
|
<span x-text="scanProgress">Running scan...</span>
|
|
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(You can navigate away - scan runs in background)</span>
|
|
</div>
|
|
|
|
<!-- Dashboard Content -->
|
|
<div x-show="!loading && !error">
|
|
<!-- Validator Type Tabs -->
|
|
<div class="mb-6">
|
|
<div class="flex flex-wrap space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1 inline-flex">
|
|
<button @click="selectValidator('all')"
|
|
:class="selectedValidator === 'all' ? 'bg-white dark:bg-gray-800 text-purple-600 dark:text-purple-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors duration-150">
|
|
All
|
|
</button>
|
|
<button @click="selectValidator('architecture')"
|
|
:class="selectedValidator === 'architecture' ? 'bg-white dark:bg-gray-800 text-purple-600 dark:text-purple-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors duration-150">
|
|
Architecture
|
|
</button>
|
|
<button @click="selectValidator('security')"
|
|
:class="selectedValidator === 'security' ? 'bg-white dark:bg-gray-800 text-red-600 dark:text-red-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors duration-150">
|
|
Security
|
|
</button>
|
|
<button @click="selectValidator('performance')"
|
|
:class="selectedValidator === 'performance' ? 'bg-white dark:bg-gray-800 text-yellow-600 dark:text-yellow-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors duration-150">
|
|
Performance
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Per-Validator Summary (shown when "All" is selected) -->
|
|
<div x-show="selectedValidator === 'all' && stats.by_validator && Object.keys(stats.by_validator).length > 0" class="grid gap-4 mb-6 md:grid-cols-3">
|
|
<template x-for="vtype in ['architecture', 'security', 'performance']" :key="vtype">
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 cursor-pointer hover:ring-2 hover:ring-purple-500"
|
|
@click="selectValidator(vtype)">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 capitalize" x-text="vtype"></p>
|
|
<p class="text-xl font-semibold text-gray-700 dark:text-gray-200"
|
|
x-text="stats.by_validator[vtype]?.total_violations || 0"></p>
|
|
</div>
|
|
<div class="p-2 rounded-full"
|
|
:class="{
|
|
'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400': vtype === 'architecture',
|
|
'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-400': vtype === 'security',
|
|
'bg-yellow-100 text-yellow-600 dark:bg-yellow-900 dark:text-yellow-400': vtype === 'performance'
|
|
}">
|
|
<span x-html="vtype === 'architecture' ? $icon('cube', 'w-5 h-5') : (vtype === 'security' ? $icon('shield-check', 'w-5 h-5') : $icon('lightning-bolt', 'w-5 h-5'))"></span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 flex space-x-3 text-xs">
|
|
<span class="text-red-600 dark:text-red-400">
|
|
<span x-text="stats.by_validator[vtype]?.errors || 0"></span> errors
|
|
</span>
|
|
<span class="text-yellow-600 dark:text-yellow-400">
|
|
<span x-text="stats.by_validator[vtype]?.warnings || 0"></span> warnings
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Card: Total Violations -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-white dark:bg-red-600">
|
|
<span x-html="$icon('exclamation', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Total Violations
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_violations">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Errors -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
|
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Errors
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.errors">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Warnings -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-yellow-500 bg-yellow-100 rounded-full dark:text-yellow-100 dark:bg-yellow-500">
|
|
<span x-html="$icon('information-circle', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Warnings
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.warnings">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Technical Debt Score -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 rounded-full"
|
|
:class="{
|
|
'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500': stats.technical_debt_score >= 80,
|
|
'text-yellow-500 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-500': stats.technical_debt_score >= 50 && stats.technical_debt_score < 80,
|
|
'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500': stats.technical_debt_score < 50
|
|
}">
|
|
<span x-html="$icon('chart-bar', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Health Score
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.technical_debt_score + '/100'">
|
|
0/100
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Cards -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Open -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Open</p>
|
|
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="stats.open">0</p>
|
|
</div>
|
|
|
|
<!-- Assigned -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Assigned</p>
|
|
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="stats.assigned">0</p>
|
|
</div>
|
|
|
|
<!-- Resolved -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Resolved</p>
|
|
<p class="text-2xl font-semibold text-green-600 dark:text-green-400" x-text="stats.resolved">0</p>
|
|
</div>
|
|
|
|
<!-- Ignored -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Ignored</p>
|
|
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="stats.ignored">0</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trend Chart and Top Files -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
|
<!-- Trend Chart -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Violation Trends (Last 7 Scans)
|
|
</h4>
|
|
<div class="h-64 flex items-center justify-center text-gray-500 dark:text-gray-400">
|
|
<template x-if="stats.trend && stats.trend.length > 0">
|
|
<div class="w-full">
|
|
<template x-for="(scan, idx) in stats.trend" :key="idx">
|
|
<div class="mb-2">
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span x-text="new Date(scan.timestamp).toLocaleDateString()"></span>
|
|
<span x-text="scan.violations"></span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
<div class="bg-purple-600 h-2 rounded-full"
|
|
:style="'width: ' + Math.min(100, (scan.violations / Math.max(...stats.trend.map(s => s.violations)) * 100)) + '%'">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template x-if="!stats.trend || stats.trend.length === 0">
|
|
<p>No scan history available</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Violating Files -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Top Violating Files
|
|
</h4>
|
|
<div class="space-y-3">
|
|
<template x-if="stats.top_files && stats.top_files.length > 0">
|
|
<template x-for="(file, idx) in stats.top_files.slice(0, 10)" :key="idx">
|
|
<div class="flex justify-between items-center">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate" x-text="file.file"></p>
|
|
</div>
|
|
<span class="ml-2 px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'bg-red-100 text-red-800': file.count >= 10,
|
|
'bg-yellow-100 text-yellow-800': file.count >= 5 && file.count < 10,
|
|
'bg-blue-100 text-blue-800': file.count < 5
|
|
}"
|
|
x-text="file.count">
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template x-if="!stats.top_files || stats.top_files.length === 0">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">No violations found</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Violations by Rule and Module -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
|
<!-- By Rule -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Top Rules Violated
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<template x-if="stats.by_rule && Object.keys(stats.by_rule).length > 0">
|
|
<template x-for="[rule_id, count] in Object.entries(stats.by_rule)" :key="rule_id">
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-gray-700 dark:text-gray-300 flex items-center">
|
|
<span class="inline-block w-2 h-2 rounded-full mr-2"
|
|
:class="{
|
|
'bg-purple-500': rule_id.startsWith('API') || rule_id.startsWith('SVC') || rule_id.startsWith('FE'),
|
|
'bg-red-500': rule_id.startsWith('SEC'),
|
|
'bg-yellow-500': rule_id.startsWith('PERF')
|
|
}"></span>
|
|
<span x-text="rule_id"></span>
|
|
</span>
|
|
<span class="font-semibold text-gray-900 dark:text-gray-100" x-text="count"></span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template x-if="!stats.by_rule || Object.keys(stats.by_rule).length === 0">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">No violations found</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- By Module -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Violations by Module
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<template x-if="stats.by_module && Object.keys(stats.by_module).length > 0">
|
|
<template x-for="[module, count] in Object.entries(stats.by_module)" :key="module">
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="module"></span>
|
|
<span class="font-semibold text-gray-900 dark:text-gray-100" x-text="count"></span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template x-if="!stats.by_module || Object.keys(stats.by_module).length === 0">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">No violations found</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="mb-8">
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Quick Actions
|
|
</h4>
|
|
<div class="flex flex-wrap gap-3">
|
|
<a :href="'/admin/code-quality/violations' + (selectedValidator !== 'all' ? '?validator_type=' + selectedValidator : '')"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
|
<span x-html="$icon('clipboard-list', 'w-4 h-4 mr-2')"></span>
|
|
View All Violations
|
|
</a>
|
|
<a :href="'/admin/code-quality/violations?status=open' + (selectedValidator !== 'all' ? '&validator_type=' + selectedValidator : '')"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:shadow-outline-gray">
|
|
<span x-html="$icon('folder-open', 'w-4 h-4 mr-2')"></span>
|
|
Open Violations
|
|
</a>
|
|
<a :href="'/admin/code-quality/violations?severity=error' + (selectedValidator !== 'all' ? '&validator_type=' + selectedValidator : '')"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:shadow-outline-gray">
|
|
<span x-html="$icon('exclamation', 'w-4 h-4 mr-2')"></span>
|
|
Errors Only
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Scan Info -->
|
|
<div x-show="stats.last_scan" class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
Last scan: <span x-text="stats.last_scan ? new Date(stats.last_scan).toLocaleString() : 'Never'"></span>
|
|
<template x-if="selectedValidator !== 'all'">
|
|
<span class="ml-2">(<span class="capitalize" x-text="selectedValidator"></span> validator)</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|