refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
{# 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 %}
|
||||
@@ -0,0 +1,342 @@
|
||||
{# app/templates/admin/code-quality-violation-detail.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Violation Detail{% endblock %}
|
||||
|
||||
{% block alpine_data %}codeQualityViolationDetail({{ violation_id }}){% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function codeQualityViolationDetail(violationId) {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'code-quality',
|
||||
violationId: violationId,
|
||||
violation: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
updating: false,
|
||||
commenting: false,
|
||||
newComment: '',
|
||||
assignUserId: '',
|
||||
resolutionNote: '',
|
||||
ignoreReason: '',
|
||||
|
||||
async init() {
|
||||
await this.loadViolation();
|
||||
},
|
||||
|
||||
async loadViolation() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/code-quality/violations/${this.violationId}`);
|
||||
this.violation = response;
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Violation');
|
||||
this.error = error.response?.data?.message || 'Failed to load violation details';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async assignViolation() {
|
||||
const userId = parseInt(this.assignUserId);
|
||||
if (!userId || isNaN(userId)) {
|
||||
Utils.showToast('Please enter a valid user ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updating = true;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/code-quality/violations/${this.violationId}/assign`, {
|
||||
user_id: userId,
|
||||
priority: 'medium'
|
||||
});
|
||||
|
||||
this.assignUserId = '';
|
||||
Utils.showToast('Violation assigned successfully', 'success');
|
||||
await this.loadViolation();
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Assign Violation');
|
||||
Utils.showToast(error.message || 'Failed to assign violation', 'error');
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
|
||||
async resolveViolation() {
|
||||
if (!this.resolutionNote.trim()) {
|
||||
Utils.showToast('Please enter a resolution note', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updating = true;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/code-quality/violations/${this.violationId}/resolve`, {
|
||||
resolution_note: this.resolutionNote
|
||||
});
|
||||
|
||||
this.resolutionNote = '';
|
||||
Utils.showToast('Violation resolved successfully', 'success');
|
||||
await this.loadViolation();
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Resolve Violation');
|
||||
Utils.showToast(error.message || 'Failed to resolve violation', 'error');
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
|
||||
async ignoreViolation() {
|
||||
if (!this.ignoreReason.trim()) {
|
||||
Utils.showToast('Please enter a reason for ignoring', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updating = true;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/code-quality/violations/${this.violationId}/ignore`, {
|
||||
reason: this.ignoreReason
|
||||
});
|
||||
|
||||
this.ignoreReason = '';
|
||||
Utils.showToast('Violation ignored successfully', 'success');
|
||||
await this.loadViolation();
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Ignore Violation');
|
||||
Utils.showToast(error.message || 'Failed to ignore violation', 'error');
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
|
||||
async addComment() {
|
||||
if (!this.newComment.trim()) return;
|
||||
|
||||
this.commenting = true;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/code-quality/violations/${this.violationId}/comments`, {
|
||||
comment: this.newComment
|
||||
});
|
||||
|
||||
this.newComment = '';
|
||||
Utils.showToast('Comment added successfully', 'success');
|
||||
await this.loadViolation();
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Add Comment');
|
||||
Utils.showToast(error.message || 'Failed to add comment', 'error');
|
||||
} finally {
|
||||
this.commenting = false;
|
||||
}
|
||||
},
|
||||
|
||||
getSeverityColor(severity) {
|
||||
return severity === 'error' ? 'red' : 'yellow';
|
||||
},
|
||||
|
||||
getStatusColor(status) {
|
||||
const colors = {
|
||||
'open': 'blue',
|
||||
'assigned': 'purple',
|
||||
'resolved': 'green',
|
||||
'ignored': 'gray'
|
||||
};
|
||||
return colors[status] || 'gray';
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
return Utils.formatDate(dateString);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Violation Details', subtitle='Review and manage architecture violation', back_url='/admin/code-quality/violations', back_label='Back to Violations') }}
|
||||
|
||||
{{ loading_state('Loading violation details...') }}
|
||||
|
||||
{{ error_state('Error loading violation') }}
|
||||
|
||||
<!-- Content -->
|
||||
<div x-show="!loading && violation" class="grid gap-6 mb-8">
|
||||
<!-- Violation Info Card -->
|
||||
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="`bg-${getSeverityColor(violation.severity)}-100 text-${getSeverityColor(violation.severity)}-800 dark:bg-${getSeverityColor(violation.severity)}-800 dark:text-${getSeverityColor(violation.severity)}-100`"
|
||||
x-text="violation.severity.toUpperCase()"></span>
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="`bg-${getStatusColor(violation.status)}-100 text-${getStatusColor(violation.status)}-800 dark:bg-${getStatusColor(violation.status)}-800 dark:text-${getStatusColor(violation.status)}-100`"
|
||||
x-text="violation.status.toUpperCase()"></span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200" x-text="violation.rule_id"></h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="violation.rule_name"></p>
|
||||
</div>
|
||||
<div class="text-right text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>ID: <span x-text="violation.id"></span></p>
|
||||
<p>Scan: <span x-text="violation.scan_id"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Violation Details -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">File Path</label>
|
||||
<code class="block px-3 py-2 bg-gray-100 dark:bg-gray-900 text-sm rounded" x-text="violation.file_path"></code>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Line Number</label>
|
||||
<p class="text-gray-900 dark:text-white" x-text="violation.line_number"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">First Detected</label>
|
||||
<p class="text-gray-900 dark:text-white" x-text="formatDate(violation.first_detected)"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Message</label>
|
||||
<p class="text-gray-900 dark:text-white" x-text="violation.message"></p>
|
||||
</div>
|
||||
|
||||
<div x-show="violation.context">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Code Context</label>
|
||||
<pre class="p-3 bg-gray-100 dark:bg-gray-900 text-sm rounded overflow-x-auto"><code x-text="violation.context"></code></pre>
|
||||
</div>
|
||||
|
||||
<div x-show="violation.suggestion">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Suggestion</label>
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-900 text-sm rounded">
|
||||
<p class="text-blue-900 dark:text-blue-100" x-text="violation.suggestion"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Management Card -->
|
||||
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">Manage Violation</h4>
|
||||
|
||||
<!-- Assign Section -->
|
||||
<div class="mb-6" x-show="violation.status === 'open' || violation.status === 'assigned'">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">Assign to User</label>
|
||||
<div class="flex gap-2">
|
||||
{# noqa: FE-008 - User ID is typed directly, not incremented #}
|
||||
<input x-model="assignUserId"
|
||||
type="number"
|
||||
placeholder="User ID"
|
||||
class="flex-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input rounded-md">
|
||||
<button @click="assignViolation()"
|
||||
:disabled="updating || !assignUserId"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!updating">Assign</span>
|
||||
<span x-show="updating">Assigning...</span>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="violation.assigned_to" class="mt-1 text-xs text-gray-500">
|
||||
Currently assigned to user ID: <span x-text="violation.assigned_to"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="grid gap-4 md:grid-cols-2" x-show="violation.status === 'open' || violation.status === 'assigned'">
|
||||
<!-- Resolve -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">Resolve Violation</label>
|
||||
<textarea x-model="resolutionNote"
|
||||
rows="2"
|
||||
placeholder="Resolution note..."
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea rounded-md"></textarea>
|
||||
<button @click="resolveViolation()"
|
||||
:disabled="updating || !resolutionNote.trim()"
|
||||
class="mt-2 w-full px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-green-600 border border-transparent rounded-lg active:bg-green-600 hover:bg-green-700 focus:outline-none focus:shadow-outline-green disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!updating">Mark as Resolved</span>
|
||||
<span x-show="updating">Resolving...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ignore -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">Ignore Violation</label>
|
||||
<textarea x-model="ignoreReason"
|
||||
rows="2"
|
||||
placeholder="Reason for ignoring..."
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea rounded-md"></textarea>
|
||||
<button @click="ignoreViolation()"
|
||||
:disabled="updating || !ignoreReason.trim()"
|
||||
class="mt-2 w-full px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-gray-600 border border-transparent rounded-lg active:bg-gray-600 hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!updating">Ignore Violation</span>
|
||||
<span x-show="updating">Ignoring...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolution Info (for resolved/ignored) -->
|
||||
<div x-show="violation.status === 'resolved' || violation.status === 'ignored'" class="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
This violation has been <span x-text="violation.status"></span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400" x-show="violation.resolution_note">
|
||||
Note: <span x-text="violation.resolution_note"></span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1" x-show="violation.resolved_at">
|
||||
<span x-text="formatDate(violation.resolved_at)"></span> by user ID <span x-text="violation.resolved_by"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div>
|
||||
<h5 class="text-md font-semibold text-gray-700 dark:text-gray-200 mb-3">Comments</h5>
|
||||
|
||||
<!-- Add Comment -->
|
||||
<div class="mb-4">
|
||||
<textarea x-model="newComment"
|
||||
rows="3"
|
||||
placeholder="Add a comment..."
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea rounded-md"></textarea>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<button @click="addComment()"
|
||||
:disabled="commenting || !newComment.trim()"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!commenting">Add Comment</span>
|
||||
<span x-show="commenting">Adding...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment List -->
|
||||
<div class="space-y-3">
|
||||
<template x-if="!violation.comments || violation.comments.length === 0">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">No comments yet</p>
|
||||
</template>
|
||||
|
||||
<template x-for="comment in (violation.comments || [])" :key="comment.id">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
User ID: <span x-text="comment.user_id"></span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500" x-text="formatDate(comment.created_at)"></p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-900 dark:text-white" x-text="comment.comment"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,192 @@
|
||||
{# app/templates/admin/code-quality-violations.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
|
||||
{% block title %}Violations List{% endblock %}
|
||||
|
||||
{% block alpine_data %}codeQualityViolations(){% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="/static/admin/js/code-quality-violations.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Code Quality Violations', subtitle='Review and manage violations across all validators', back_url='/admin/code-quality', back_label='Back to Dashboard') }}
|
||||
|
||||
{{ loading_state('Loading violations...') }}
|
||||
|
||||
{{ error_state('Error loading violations') }}
|
||||
|
||||
<!-- Content -->
|
||||
<div x-show="!loading">
|
||||
<!-- Filters -->
|
||||
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Filters</h3>
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<!-- Validator Type Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Validator
|
||||
</label>
|
||||
<select x-model="filters.validator_type"
|
||||
@change="applyFilters()"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select rounded-md">
|
||||
<option value="">All Validators</option>
|
||||
<option value="architecture">Architecture</option>
|
||||
<option value="security">Security</option>
|
||||
<option value="performance">Performance</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Severity Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Severity
|
||||
</label>
|
||||
<select x-model="filters.severity"
|
||||
@change="applyFilters()"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select x-model="filters.status"
|
||||
@change="applyFilters()"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="assigned">Assigned</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="ignored">Ignored</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Rule ID Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Rule ID
|
||||
</label>
|
||||
<input x-model="filters.rule_id"
|
||||
@input.debounce.500ms="applyFilters()"
|
||||
type="text"
|
||||
placeholder="e.g. SEC-001"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input rounded-md">
|
||||
</div>
|
||||
|
||||
<!-- File Path Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
File Path
|
||||
</label>
|
||||
<input x-model="filters.file_path"
|
||||
@input.debounce.500ms="applyFilters()"
|
||||
type="text"
|
||||
placeholder="e.g. app/api"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Violations Table -->
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Validator', 'Rule', 'Severity', 'File', 'Line', 'Message', 'Status', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="violations.length === 0">
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
No violations found
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="violation in violations" :key="violation.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<!-- Validator Type Badge -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full capitalize"
|
||||
:class="{
|
||||
'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100': violation.validator_type === 'architecture',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': violation.validator_type === 'security',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': violation.validator_type === 'performance'
|
||||
}"
|
||||
x-text="violation.validator_type">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Rule ID -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div>
|
||||
<p class="font-semibold" x-text="violation.rule_id"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Severity Badge -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': violation.severity === 'error',
|
||||
'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700': violation.severity === 'warning',
|
||||
'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700': violation.severity === 'info'
|
||||
}"
|
||||
x-text="violation.severity">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- File Path -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<p class="truncate max-w-xs" :title="violation.file_path" x-text="violation.file_path"></p>
|
||||
</td>
|
||||
|
||||
<!-- Line Number -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<p x-text="violation.line_number"></p>
|
||||
</td>
|
||||
|
||||
<!-- Message -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<p class="truncate max-w-md" :title="violation.message" x-text="violation.message"></p>
|
||||
</td>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700': violation.status === 'open',
|
||||
'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700': violation.status === 'assigned',
|
||||
'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700': violation.status === 'resolved',
|
||||
'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700': violation.status === 'ignored'
|
||||
}"
|
||||
x-text="violation.status">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a :href="'/admin/code-quality/violations/' + violation.id"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View Details">
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
|
||||
{{ pagination() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
3177
app/modules/dev_tools/templates/dev_tools/admin/components.html
Normal file
3177
app/modules/dev_tools/templates/dev_tools/admin/components.html
Normal file
File diff suppressed because it is too large
Load Diff
322
app/modules/dev_tools/templates/dev_tools/admin/icons.html
Normal file
322
app/modules/dev_tools/templates/dev_tools/admin/icons.html
Normal file
@@ -0,0 +1,322 @@
|
||||
{# app/templates/admin/icons.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
|
||||
{% block title %}Icons Browser{% endblock %}
|
||||
|
||||
{# ✅ CRITICAL: Link to Alpine.js component #}
|
||||
{% block alpine_data %}adminIcons(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Icons Browser', back_url='/admin/dashboard', back_label='Back to Dashboard') }}
|
||||
|
||||
<!-- Introduction Card -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-lg shadow-lg p-6 mb-8">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('photograph', 'w-12 h-12')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-xl font-bold mb-2 text-gray-700 dark:text-gray-200">Icon Library</h3>
|
||||
<p class="text-gray-700 dark:text-gray-200 opacity-90">
|
||||
Browse all <span x-text="allIcons.length"></span> available icons. Click any icon to copy its name or
|
||||
usage code.
|
||||
</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-700 dark:text-gray-200 opacity-90">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 mr-1')"></span>
|
||||
<span>Heroicons</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 mr-1')"></span>
|
||||
<span>Dark Mode Support</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 mr-1')"></span>
|
||||
<span>Fully Accessible</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<!-- Search Box -->
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Search Icons
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="searchQuery"
|
||||
@input="filterIcons()"
|
||||
placeholder="Type to search... (e.g., 'user', 'arrow', 'check')"
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Found <span x-text="filteredIcons.length"></span> icon(s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Category Pills -->
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Filter by Category
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="category in categories.slice(0, 6)" :key="category.id">
|
||||
<button
|
||||
@click="setCategory(category.id)"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-full transition-colors"
|
||||
:class="activeCategory === category.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'"
|
||||
>
|
||||
<span x-html="$icon(category.icon, 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="category.name"></span>
|
||||
<span class="ml-1 opacity-75" x-text="'(' + getCategoryCount(category.id) + ')'"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All Categories (Expandable) -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<details class="group">
|
||||
<summary
|
||||
class="cursor-pointer text-sm font-medium text-purple-600 dark:text-purple-400 hover:text-purple-700 flex items-center">
|
||||
<span x-html="$icon('chevron-right', 'w-4 h-4 mr-1 group-open:rotate-90 transition-transform')"></span>
|
||||
Show All Categories
|
||||
</summary>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<template x-for="category in categories" :key="category.id">
|
||||
<button
|
||||
@click="setCategory(category.id)"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg transition-colors"
|
||||
:class="activeCategory === category.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'"
|
||||
>
|
||||
<span x-html="$icon(category.icon, 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="category.name"></span>
|
||||
<span class="ml-2 px-2 py-0.5 bg-black bg-opacity-10 rounded-full"
|
||||
x-text="getCategoryCount(category.id)"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Category Info -->
|
||||
<div x-show="activeCategory !== 'all'"
|
||||
class="mb-4 flex items-center justify-between bg-purple-50 dark:bg-purple-900 dark:bg-opacity-20 border border-purple-200 dark:border-purple-700 rounded-lg px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon(getCategoryInfo(activeCategory).icon, 'w-5 h-5 text-purple-600 dark:text-purple-400 mr-2')"></span>
|
||||
<span class="text-sm font-medium text-purple-900 dark:text-purple-200">
|
||||
Showing <span x-text="getCategoryInfo(activeCategory).name"></span>
|
||||
(<span x-text="filteredIcons.length"></span> icons)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="setCategory('all')"
|
||||
class="text-sm text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 flex items-center"
|
||||
>
|
||||
<span x-html="$icon('close', 'w-4 h-4 mr-1')"></span>
|
||||
Clear Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Icons Grid -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<!-- Empty State -->
|
||||
<div x-show="filteredIcons.length === 0" class="text-center py-12">
|
||||
<span x-html="$icon('exclamation', 'w-16 h-16 mx-auto text-gray-400 mb-4')"></span>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">No icons found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Try adjusting your search or filter</p>
|
||||
<button
|
||||
@click="searchQuery = ''; setCategory('all')"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Icons Grid -->
|
||||
<div x-show="filteredIcons.length > 0"
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3">
|
||||
<template x-for="icon in filteredIcons" :key="icon.name">
|
||||
<div
|
||||
@click="selectIcon(icon.name)"
|
||||
class="group relative flex flex-col items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 rounded-lg hover:bg-purple-50 dark:hover:bg-purple-900 dark:hover:bg-opacity-20 cursor-pointer transition-all hover:shadow-md border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700"
|
||||
:class="{ 'border-purple-500 bg-purple-50 dark:bg-purple-900 dark:bg-opacity-30': selectedIcon === icon.name }"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<div class="text-gray-600 dark:text-gray-400 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
|
||||
<span x-html="$icon(icon.name, 'w-8 h-8')"></span>
|
||||
</div>
|
||||
|
||||
<!-- Icon Name -->
|
||||
<p class="mt-2 text-xs text-center text-gray-600 dark:text-gray-400 font-mono truncate w-full px-1"
|
||||
:title="icon.name" x-text="icon.name"></p>
|
||||
|
||||
<!-- Hover Actions -->
|
||||
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-white dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90 rounded-lg">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
@click.stop="copyIconName(icon.name)"
|
||||
class="p-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors"
|
||||
title="Copy name"
|
||||
>
|
||||
<span x-html="$icon('duplicate', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click.stop="copyIconUsage(icon.name)"
|
||||
class="p-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors"
|
||||
title="Copy usage"
|
||||
>
|
||||
<span x-html="$icon('code', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Icon Details -->
|
||||
<div x-show="selectedIcon" class="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">
|
||||
Selected Icon: <span class="font-mono text-purple-600 dark:text-purple-400" x-text="selectedIcon"></span>
|
||||
</h3>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<!-- Preview -->
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Preview</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-8 flex items-center justify-center gap-6">
|
||||
<div class="text-gray-800 dark:text-gray-200">
|
||||
<span x-html="$icon(selectedIcon, 'w-12 h-12')"></span>
|
||||
</div>
|
||||
<div class="text-gray-800 dark:text-gray-200">
|
||||
<span x-html="$icon(selectedIcon, 'w-16 h-16')"></span>
|
||||
</div>
|
||||
<div class="text-gray-800 dark:text-gray-200">
|
||||
<span x-html="$icon(selectedIcon, 'w-24 h-24')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Code -->
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Usage Code</h4>
|
||||
|
||||
<!-- Alpine.js Usage -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Alpine.js
|
||||
(Recommended)</label>
|
||||
<div class="relative">
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-3 text-xs overflow-x-auto"><code
|
||||
x-text="'x-html="$icon(\'' + selectedIcon + '\', \'w-5 h-5\')"'"></code></pre>
|
||||
<button
|
||||
@click="copyIconUsage(selectedIcon)"
|
||||
class="absolute top-2 right-2 p-1.5 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<span x-html="$icon('duplicate', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon Name -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Icon Name</label>
|
||||
<div class="relative">
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-3 text-xs overflow-x-auto"><code
|
||||
x-text="selectedIcon"></code></pre>
|
||||
<button
|
||||
@click="copyIconName(selectedIcon)"
|
||||
class="absolute top-2 right-2 p-1.5 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<span x-html="$icon('duplicate', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size Examples -->
|
||||
<div class="mt-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Common Sizes</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="text-center">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-1">
|
||||
<span x-html="$icon(selectedIcon, 'w-4 h-4')"></span>
|
||||
</div>
|
||||
<code class="text-xs text-gray-600 dark:text-gray-400">w-4 h-4</code>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-1">
|
||||
<span x-html="$icon(selectedIcon, 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<code class="text-xs text-gray-600 dark:text-gray-400">w-5 h-5</code>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-1">
|
||||
<span x-html="$icon(selectedIcon, 'w-6 h-6')"></span>
|
||||
</div>
|
||||
<code class="text-xs text-gray-600 dark:text-gray-400">w-6 h-6</code>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-1">
|
||||
<span x-html="$icon(selectedIcon, 'w-8 h-8')"></span>
|
||||
</div>
|
||||
<code class="text-xs text-gray-600 dark:text-gray-400">w-8 h-8</code>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-1">
|
||||
<span x-html="$icon(selectedIcon, 'w-12 h-12')"></span>
|
||||
</div>
|
||||
<code class="text-xs text-gray-600 dark:text-gray-400">w-12 h-12</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Guide -->
|
||||
<div class="mb-8 mt-6 bg-blue-50 dark:bg-gray-800 border border-blue-200 dark:border-gray-700 rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
|
||||
<span x-html="$icon('book-open', 'w-5 h-5 mr-2 text-blue-600')"></span>
|
||||
How to Use Icons
|
||||
</h3>
|
||||
<div class="grid md:grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">In Alpine.js Templates</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-2">Use the <code
|
||||
class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">x-html</code> directive:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-2 text-xs overflow-x-auto"><code><span x-html="$icon('home', 'w-5 h-5')"></span></code></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">Customizing Size & Color</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-2">Use Tailwind classes:</p>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded p-2 text-xs overflow-x-auto"><code><span x-html="$icon('check', 'w-6 h-6 text-green-500')"></span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{# ✅ CRITICAL: Load JavaScript file #}
|
||||
<script src="{{ url_for('dev_tools_static', path='admin/js/icons-page.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,315 @@
|
||||
{# app/templates/admin/test-auth-flow.html #}
|
||||
{% extends 'admin/base.html' %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
|
||||
{% block title %}Auth Flow Testing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="authFlowTest()" x-init="init()">
|
||||
{{ page_header('Auth Flow Testing', subtitle='Comprehensive testing for Jinja2 migration auth loop fix') }}
|
||||
|
||||
{# Log Level Control #}
|
||||
<div class="px-4 py-3 mb-6 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg shadow-md border-l-4 border-yellow-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-yellow-800 dark:text-yellow-200">Log Level Control</h4>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
|
||||
Change logging verbosity for login.js and api-client.js
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="setLogLevel(0)" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">0 - None</button>
|
||||
<button @click="setLogLevel(1)" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">1 - Errors</button>
|
||||
<button @click="setLogLevel(2)" class="px-3 py-1 text-xs font-medium text-white bg-yellow-600 rounded hover:bg-yellow-700">2 - Warnings</button>
|
||||
<button @click="setLogLevel(3)" class="px-3 py-1 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700">3 - Info</button>
|
||||
<button @click="setLogLevel(4)" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">4 - Debug</button>
|
||||
</div>
|
||||
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2 italic">
|
||||
Current: LOGIN = <span x-text="currentLoginLevel">4</span>, API = <span x-text="currentApiLevel">3</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Test Sections Grid #}
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
{# Test 1: Clean Slate #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-blue-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 1: Clean Slate - Fresh Login
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests complete login flow from scratch with no existing tokens.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Clear All Data</li>
|
||||
<li>Navigate to /admin</li>
|
||||
<li>Should land on login page</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: Single redirect /admin -> /admin/login, no loops</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="clearAllData()" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">Clear All Data</button>
|
||||
<button @click="navigateTo('/admin')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to /admin</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test 2: Successful Login #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-green-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 2: Successful Login
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests that login works correctly and redirects to dashboard.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Go to /admin/login</li>
|
||||
<li>Enter valid admin credentials</li>
|
||||
<li>Click Login</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: Token stored, redirect to /admin/dashboard</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="navigateTo('/admin/login')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Login</button>
|
||||
<button @click="checkAuthStatus()" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">Check Status</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test 3: Dashboard Refresh #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-purple-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 3: Dashboard Refresh
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests that refreshing dashboard works without redirect loops.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Complete Test 2 (login)</li>
|
||||
<li>Press F5 or click Refresh</li>
|
||||
<li>Dashboard should reload normally</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: No redirect to login, stats load correctly</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
|
||||
<button @click="window.location.reload()" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">Refresh Page</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test 4: Expired Token #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-orange-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 4: Expired Token Handling
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests that expired tokens are handled gracefully.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Set Expired Token</li>
|
||||
<li>Navigate to Dashboard</li>
|
||||
<li>Should redirect to login</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: 401 response, redirect to login, no loops</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="setExpiredToken()" class="px-3 py-1 text-xs font-medium text-white bg-orange-600 rounded hover:bg-orange-700">Set Expired Token</button>
|
||||
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test 5: Direct Access (No Token) #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-red-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 5: Direct Access (Unauthenticated)
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests accessing dashboard without token redirects to login.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Clear All Data</li>
|
||||
<li>Navigate to Dashboard</li>
|
||||
<li>Should redirect to login</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: Redirect to /admin/login, no API calls</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="clearAllData()" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">Clear All Data</button>
|
||||
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test 6: Login with Valid Token #}
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-teal-500">
|
||||
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 6: Login Page with Valid Token
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Tests visiting login page while already authenticated.
|
||||
</p>
|
||||
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Login successfully (Test 2)</li>
|
||||
<li>Click Go to Login Page</li>
|
||||
<li>Token should be cleared</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
|
||||
<p class="text-xs text-green-700 dark:text-green-400">Expected: Token cleared, form displayed, no loops</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="setMockToken()" class="px-3 py-1 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700">Set Mock Token</button>
|
||||
<button @click="navigateTo('/admin/login')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Status Panel #}
|
||||
<div class="px-4 py-3 bg-gray-800 rounded-lg shadow-md">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-lg font-semibold text-gray-200">Current Auth Status</h4>
|
||||
<button @click="updateStatus()" class="px-3 py-1 text-xs text-gray-400 border border-gray-600 rounded hover:bg-gray-700">Refresh</button>
|
||||
</div>
|
||||
<div class="font-mono text-sm space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Current URL:</span>
|
||||
<span class="text-blue-400" x-text="currentUrl">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Has admin_token:</span>
|
||||
<span :class="hasToken ? 'text-green-400' : 'text-red-400'" x-text="hasToken ? 'Yes' : 'No'">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Has admin_user:</span>
|
||||
<span :class="hasUser ? 'text-green-400' : 'text-red-400'" x-text="hasUser ? 'Yes' : 'No'">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Token Preview:</span>
|
||||
<span class="text-green-400 truncate max-w-xs" x-text="tokenPreview">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Username:</span>
|
||||
<span class="text-green-400" x-text="username">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Warning Box #}
|
||||
<div class="mt-6 px-4 py-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<h4 class="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">Important Notes</h4>
|
||||
<ul class="list-disc list-inside text-sm text-red-600 dark:text-red-400 space-y-1">
|
||||
<li>Always check browser console for detailed logs</li>
|
||||
<li>Use Network tab to see actual HTTP requests and redirects</li>
|
||||
<li>Clear browser cache if you see unexpected behavior</li>
|
||||
<li>Make sure FastAPI server is running on localhost:8000</li>
|
||||
<li>Valid admin credentials required for login tests</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function authFlowTest() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'auth-testing',
|
||||
|
||||
currentUrl: '-',
|
||||
hasToken: false,
|
||||
hasUser: false,
|
||||
tokenPreview: '-',
|
||||
username: '-',
|
||||
currentLoginLevel: 4,
|
||||
currentApiLevel: 3,
|
||||
|
||||
init() {
|
||||
this.updateStatus();
|
||||
setInterval(() => this.updateStatus(), 2000);
|
||||
console.log('Auth Flow Testing Script Loaded');
|
||||
},
|
||||
|
||||
updateStatus() {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const userStr = localStorage.getItem('admin_user');
|
||||
let user = null;
|
||||
|
||||
try {
|
||||
user = userStr ? JSON.parse(userStr) : null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user data:', e);
|
||||
}
|
||||
|
||||
this.currentUrl = window.location.href;
|
||||
this.hasToken = !!token;
|
||||
this.hasUser = !!user;
|
||||
this.tokenPreview = token ? token.substring(0, 30) + '...' : 'No token';
|
||||
this.username = user?.username || 'Not logged in';
|
||||
},
|
||||
|
||||
clearAllData() {
|
||||
console.log('Clearing all localStorage data...');
|
||||
localStorage.clear();
|
||||
console.log('All data cleared');
|
||||
alert('All localStorage data cleared!');
|
||||
this.updateStatus();
|
||||
},
|
||||
|
||||
navigateTo(path) {
|
||||
console.log(`Navigating to ${path}...`);
|
||||
window.location.href = path;
|
||||
},
|
||||
|
||||
checkAuthStatus() {
|
||||
this.updateStatus();
|
||||
alert('Check console and status panel for auth details.');
|
||||
},
|
||||
|
||||
setExpiredToken() {
|
||||
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNTE2MjM5MDIyfQ.invalid';
|
||||
localStorage.setItem('admin_token', expiredToken);
|
||||
localStorage.setItem('admin_user', JSON.stringify({
|
||||
id: 1,
|
||||
username: 'test_expired',
|
||||
role: 'admin'
|
||||
}));
|
||||
alert('Expired token set! Now try navigating to dashboard.');
|
||||
this.updateStatus();
|
||||
},
|
||||
|
||||
setMockToken() {
|
||||
const mockToken = 'mock_valid_token_' + Date.now();
|
||||
localStorage.setItem('admin_token', mockToken);
|
||||
localStorage.setItem('admin_user', JSON.stringify({
|
||||
id: 1,
|
||||
username: 'test_user',
|
||||
role: 'admin'
|
||||
}));
|
||||
alert('Mock token set! Note: This won\'t work with real backend.');
|
||||
this.updateStatus();
|
||||
},
|
||||
|
||||
setLogLevel(level) {
|
||||
if (typeof window.LOG_LEVEL !== 'undefined') {
|
||||
window.LOG_LEVEL = level;
|
||||
this.currentLoginLevel = level;
|
||||
}
|
||||
if (typeof window.API_LOG_LEVEL !== 'undefined') {
|
||||
window.API_LOG_LEVEL = level;
|
||||
this.currentApiLevel = level;
|
||||
}
|
||||
alert(`Log level set to ${level}. Reload to apply to all scripts.`);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,206 @@
|
||||
{# app/templates/admin/test-vendors-users-migration.html #}
|
||||
{% extends 'admin/base.html' %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
|
||||
{% block title %}Vendors & Users Migration Testing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="migrationTest()" x-init="init()">
|
||||
{{ page_header('Vendors & Users Migration Testing', subtitle='Comprehensive test suite for verifying the Jinja2 migration') }}
|
||||
|
||||
{# Status Cards #}
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
||||
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Dashboard</p>
|
||||
<p class="text-lg font-semibold text-green-600 dark:text-green-400">Complete</p>
|
||||
</div>
|
||||
</div>
|
||||
<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('building-storefront', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Vendors List</p>
|
||||
<p class="text-lg font-semibold" :class="vendorsStatus === 'Complete' ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'" x-text="vendorsStatus">Testing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('pencil-square', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Vendor Edit</p>
|
||||
<p class="text-lg font-semibold" :class="editStatus === 'Complete' ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'" x-text="editStatus">Testing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Users Page</p>
|
||||
<p class="text-lg font-semibold" :class="usersStatus === 'Complete' ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'" x-text="usersStatus">Testing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Quick Actions #}
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-600 dark:text-gray-300">Quick Actions</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="{{ url_for('admin:vendors_list') }}" class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-orange-600 border border-transparent rounded-lg hover:bg-orange-700 focus:outline-none">
|
||||
<span x-html="$icon('building-storefront', 'w-4 h-4 mr-2')"></span>
|
||||
Go to Vendors List
|
||||
</a>
|
||||
<a href="{{ url_for('admin:users_list') }}" class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-green-600 border border-transparent rounded-lg hover:bg-green-700 focus:outline-none">
|
||||
<span x-html="$icon('user-group', 'w-4 h-4 mr-2')"></span>
|
||||
Go to Users Page
|
||||
</a>
|
||||
<a href="{{ url_for('admin:dashboard') }}" 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">
|
||||
<span x-html="$icon('home', 'w-4 h-4 mr-2')"></span>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Progress Bar #}
|
||||
<div class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-green-600 h-2.5 rounded-full transition-all duration-300" :style="'width: ' + progress + '%'"></div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400" x-text="progress + '% Complete - ' + checkedCount + '/' + totalChecks + ' checks passed'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test Section: Vendors List #}
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-orange-500">
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 1: Vendors List Page
|
||||
<span class="ml-2 px-2 py-1 text-xs font-semibold text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">High Priority</span>
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">Tests the vendor LIST functionality using adminVendors() function.</p>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<template x-for="(item, index) in vendorChecks" :key="index">
|
||||
<label class="flex items-center p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" x-model="item.checked" @change="updateProgress()" class="w-4 h-4 text-purple-600 form-checkbox focus:ring-purple-500">
|
||||
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300" :class="{ 'line-through text-gray-400': item.checked }" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border-l-3 border-green-500">
|
||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">Expected: adminVendors() works with ApiClient, Logger, Utils</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Test Section: Users Page #}
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-green-500">
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-600 dark:text-gray-300">
|
||||
Test 2: Users Page
|
||||
<span class="ml-2 px-2 py-1 text-xs font-semibold text-yellow-700 bg-yellow-100 rounded-full dark:bg-yellow-700 dark:text-yellow-100">Medium Priority</span>
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">Tests the users page created with adminUsers() function.</p>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<template x-for="(item, index) in userChecks" :key="index">
|
||||
<label class="flex items-center p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" x-model="item.checked" @change="updateProgress()" class="w-4 h-4 text-purple-600 form-checkbox focus:ring-purple-500">
|
||||
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300" :class="{ 'line-through text-gray-400': item.checked }" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border-l-3 border-green-500">
|
||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">Expected: adminUsers() follows same pattern as vendors list</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Console Panel #}
|
||||
<div class="px-4 py-3 bg-gray-800 rounded-lg shadow-md">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-lg font-semibold text-gray-200">Test Console</h4>
|
||||
<button @click="logs = []" class="px-3 py-1 text-xs text-gray-400 border border-gray-600 rounded hover:bg-gray-700">Clear</button>
|
||||
</div>
|
||||
<div class="h-48 overflow-y-auto font-mono text-sm">
|
||||
<template x-for="(log, index) in logs" :key="index">
|
||||
<div class="py-1 border-b border-gray-700">
|
||||
<span class="text-gray-500" x-text="log.time"></span>
|
||||
<span :class="log.level === 'success' ? 'text-green-400' : log.level === 'error' ? 'text-red-400' : 'text-blue-400'" x-text="log.message"></span>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="logs.length === 0" class="text-gray-500">No logs yet. Start testing!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function migrationTest() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'testing',
|
||||
|
||||
vendorsStatus: 'Testing',
|
||||
editStatus: 'Testing',
|
||||
usersStatus: 'Testing',
|
||||
progress: 0,
|
||||
checkedCount: 0,
|
||||
totalChecks: 0,
|
||||
logs: [],
|
||||
|
||||
vendorChecks: [
|
||||
{ label: 'Navigate to /admin/vendors - Page loads without errors', checked: false },
|
||||
{ label: 'Check console - No JavaScript errors', checked: false },
|
||||
{ label: 'Verify Alpine.js function - adminVendors() is called', checked: false },
|
||||
{ label: '4 stat cards display in a grid', checked: false },
|
||||
{ label: 'Table displays with correct headers', checked: false },
|
||||
{ label: 'Status badges show correctly (Verified/Pending)', checked: false },
|
||||
{ label: 'Action buttons (View, Edit, Delete) work', checked: false },
|
||||
],
|
||||
|
||||
userChecks: [
|
||||
{ label: 'Navigate to /admin/users - Page loads without errors', checked: false },
|
||||
{ label: 'Alpine.js adminUsers() function works', checked: false },
|
||||
{ label: '4 stat cards display (Total, Active, Admins, Vendors)', checked: false },
|
||||
{ label: 'Users table displays correctly', checked: false },
|
||||
{ label: 'Role badges display (admin/vendor/customer)', checked: false },
|
||||
{ label: 'Status badges show (Active/Inactive)', checked: false },
|
||||
{ label: 'Action buttons work (View, Edit, Toggle Status)', checked: false },
|
||||
],
|
||||
|
||||
init() {
|
||||
this.totalChecks = this.vendorChecks.length + this.userChecks.length;
|
||||
this.log('Migration Test Suite Ready', 'success');
|
||||
this.log('Follow the test sections to verify migration', 'info');
|
||||
},
|
||||
|
||||
updateProgress() {
|
||||
const vendorChecked = this.vendorChecks.filter(c => c.checked).length;
|
||||
const userChecked = this.userChecks.filter(c => c.checked).length;
|
||||
this.checkedCount = vendorChecked + userChecked;
|
||||
this.progress = Math.round((this.checkedCount / this.totalChecks) * 100);
|
||||
|
||||
// Update section status
|
||||
if (vendorChecked === this.vendorChecks.length) {
|
||||
this.vendorsStatus = 'Complete';
|
||||
this.log('Vendors list tests completed!', 'success');
|
||||
}
|
||||
if (userChecked === this.userChecks.length) {
|
||||
this.usersStatus = 'Complete';
|
||||
this.log('Users page tests completed!', 'success');
|
||||
}
|
||||
},
|
||||
|
||||
log(message, level = 'info') {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
this.logs.push({ time: `[${time}]`, message, level });
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,419 @@
|
||||
{# app/templates/admin/testing-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, action_button %}
|
||||
|
||||
{% block title %}Testing Dashboard{% endblock %}
|
||||
|
||||
{% block alpine_data %}testingDashboard(){% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="/static/admin/js/testing-dashboard.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Testing Dashboard', subtitle='pytest results and test coverage') %}
|
||||
{{ refresh_button(variant='secondary') }}
|
||||
{{ action_button('Run Tests', 'Running...', 'running', 'runTests()', icon='play') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading test results...') }}
|
||||
|
||||
{{ error_state('Error loading test results') }}
|
||||
|
||||
{{ alert_dynamic(type='success', message_var='successMessage', show_condition='successMessage') }}
|
||||
|
||||
<!-- Running Indicator -->
|
||||
<div x-show="running" x-cloak class="mb-6">
|
||||
<div class="p-4 bg-purple-100 dark:bg-purple-900 rounded-lg shadow-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'h-5 w-5 text-purple-600 dark:text-purple-400 mr-3')"></span>
|
||||
<div>
|
||||
<p class="font-semibold text-purple-800 dark:text-purple-200">Running tests...</p>
|
||||
<p class="text-sm text-purple-600 dark:text-purple-300">Tests are executing in the background. You can leave this page and come back.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-2xl font-bold text-purple-700 dark:text-purple-300" x-text="formatDuration(elapsedTime)">0s</p>
|
||||
<p class="text-xs text-purple-600 dark:text-purple-400">elapsed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="w-full bg-purple-200 dark:bg-purple-800 rounded-full h-1.5 overflow-hidden">
|
||||
<div class="bg-purple-600 dark:bg-purple-400 h-1.5 rounded-full animate-pulse" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div x-show="!loading && !error">
|
||||
<!-- Stats Cards Row 1 - Main Metrics -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Tests -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('beaker', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Tests
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_tests">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Passed -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Passed
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.passed">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Failed -->
|
||||
<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-red-100 dark:bg-red-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">
|
||||
Failed
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.failed + stats.errors">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Pass Rate -->
|
||||
<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.pass_rate >= 90,
|
||||
'text-yellow-500 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-500': stats.pass_rate >= 70 && stats.pass_rate < 90,
|
||||
'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500': stats.pass_rate < 70
|
||||
}">
|
||||
<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">
|
||||
Pass Rate
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pass_rate + '%'">
|
||||
0%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards Row 2 - Secondary Metrics -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Skipped -->
|
||||
<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">Skipped</p>
|
||||
<p class="text-2xl font-semibold text-yellow-600 dark:text-yellow-400" x-text="stats.skipped">0</p>
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<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">Duration</p>
|
||||
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="formatDuration(stats.duration_seconds)">0s</p>
|
||||
</div>
|
||||
|
||||
<!-- Coverage -->
|
||||
<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">Coverage</p>
|
||||
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="stats.coverage_percent ? stats.coverage_percent + '%' : 'N/A'">N/A</p>
|
||||
</div>
|
||||
|
||||
<!-- Last Run Status -->
|
||||
<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">Status</p>
|
||||
<p class="text-2xl font-semibold"
|
||||
:class="{
|
||||
'text-green-600 dark:text-green-400': stats.last_run_status === 'passed',
|
||||
'text-red-600 dark:text-red-400': stats.last_run_status === 'failed',
|
||||
'text-yellow-600 dark:text-yellow-400': stats.last_run_status === 'running',
|
||||
'text-gray-600 dark:text-gray-400': !stats.last_run_status
|
||||
}"
|
||||
x-text="stats.last_run_status ? stats.last_run_status.toUpperCase() : 'NO RUNS'">
|
||||
NO RUNS
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Collection Stats -->
|
||||
<div class="mb-8 p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Test Collection
|
||||
</h4>
|
||||
<span x-show="stats.last_collected" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Last collected: <span x-text="stats.last_collected ? new Date(stats.last_collected).toLocaleString() : 'Never'"></span>
|
||||
</span>
|
||||
</div>
|
||||
<template x-if="stats.collected_tests > 0">
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<!-- Total Collected -->
|
||||
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p class="text-2xl font-bold text-gray-700 dark:text-gray-200" x-text="stats.collected_tests">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Total Tests</p>
|
||||
</div>
|
||||
<!-- Unit Tests -->
|
||||
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
|
||||
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="stats.unit_tests">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Unit</p>
|
||||
</div>
|
||||
<!-- Integration Tests -->
|
||||
<div class="text-center p-3 bg-purple-50 dark:bg-purple-900/30 rounded-lg">
|
||||
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="stats.integration_tests">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Integration</p>
|
||||
</div>
|
||||
<!-- Performance Tests -->
|
||||
<div class="text-center p-3 bg-orange-50 dark:bg-orange-900/30 rounded-lg">
|
||||
<p class="text-2xl font-bold text-orange-600 dark:text-orange-400" x-text="stats.performance_tests">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Performance</p>
|
||||
</div>
|
||||
<!-- Test Files -->
|
||||
<div class="text-center p-3 bg-green-50 dark:bg-green-900/30 rounded-lg">
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="stats.total_test_files">0</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Files</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="stats.collected_tests === 0">
|
||||
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('collection', 'w-8 h-8 mx-auto mb-2')"></span>
|
||||
<p class="text-sm">No collection data. Click "Collect Tests" to discover available tests.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Trend Chart and Tests by Category -->
|
||||
<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">
|
||||
Pass Rate Trend (Last 10 Runs)
|
||||
</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="(run, idx) in stats.trend" :key="idx">
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span x-text="new Date(run.timestamp).toLocaleDateString()"></span>
|
||||
<span>
|
||||
<span x-text="run.passed"></span>/<span x-text="run.total"></span>
|
||||
(<span x-text="run.pass_rate"></span>%)
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div class="h-2 rounded-full transition-all duration-300"
|
||||
:class="{
|
||||
'bg-green-500': run.pass_rate >= 90,
|
||||
'bg-yellow-500': run.pass_rate >= 70 && run.pass_rate < 90,
|
||||
'bg-red-500': run.pass_rate < 70
|
||||
}"
|
||||
:style="'width: ' + run.pass_rate + '%'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!stats.trend || stats.trend.length === 0">
|
||||
<div class="text-center">
|
||||
<span x-html="$icon('beaker', 'w-12 h-12 mx-auto mb-2 text-gray-400')"></span>
|
||||
<p>No test runs yet</p>
|
||||
<p class="text-sm">Run tests to see trend data</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tests by Category -->
|
||||
<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">
|
||||
Tests by Category
|
||||
</h4>
|
||||
<div class="space-y-3">
|
||||
<template x-if="stats.by_category && Object.keys(stats.by_category).length > 0">
|
||||
<template x-for="[category, data] in Object.entries(stats.by_category)" :key="category">
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="category"></span>
|
||||
<span class="font-semibold">
|
||||
<span class="text-green-600" x-text="data.passed"></span>
|
||||
/
|
||||
<span x-text="data.total"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div class="bg-green-500 h-2 rounded-full"
|
||||
:style="'width: ' + (data.total > 0 ? (data.passed / data.total * 100) : 0) + '%'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!stats.by_category || Object.keys(stats.by_category).length === 0">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No category data available</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Failing Tests -->
|
||||
<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">
|
||||
Top Failing Tests
|
||||
</h4>
|
||||
<template x-if="stats.top_failing && stats.top_failing.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Test Name</th>
|
||||
<th class="px-4 py-3">File</th>
|
||||
<th class="px-4 py-3 text-right">Failures</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="test in stats.top_failing" :key="test.test_name + test.test_file">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-semibold" x-text="test.test_name"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 truncate max-w-xs" x-text="test.test_file"></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<span class="px-2 py-1 text-xs font-semibold text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100"
|
||||
x-text="test.failure_count">
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!stats.top_failing || stats.top_failing.length === 0">
|
||||
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('check-circle', 'w-12 h-12 mx-auto mb-2 text-green-500')"></span>
|
||||
<p class="font-medium">No failing tests!</p>
|
||||
<p class="text-sm">All tests are passing</p>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<button @click="runTests('tests/unit')"
|
||||
:disabled="running"
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-html="$icon('beaker', 'w-4 h-4 mr-2')"></span>
|
||||
Run Unit Tests
|
||||
</button>
|
||||
<button @click="runTests('tests/integration')"
|
||||
:disabled="running"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-html="$icon('server', 'w-4 h-4 mr-2')"></span>
|
||||
Run Integration Tests
|
||||
</button>
|
||||
<button @click="collectTests()"
|
||||
:disabled="collecting"
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-html="$icon('collection', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="collecting ? 'Collecting...' : 'Collect Tests'"></span>
|
||||
</button>
|
||||
<a href="/admin/testing-hub"
|
||||
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">
|
||||
<span x-html="$icon('clipboard-list', 'w-4 h-4 mr-2')"></span>
|
||||
Manual Testing
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Runs -->
|
||||
<div class="mb-8" x-show="runs.length > 0">
|
||||
<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">
|
||||
Recent Test Runs
|
||||
</h4>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Path</th>
|
||||
<th class="px-4 py-3 text-center">Total</th>
|
||||
<th class="px-4 py-3 text-center">Passed</th>
|
||||
<th class="px-4 py-3 text-center">Failed</th>
|
||||
<th class="px-4 py-3 text-center">Pass Rate</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="run in runs" :key="run.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-3 text-sm" x-text="new Date(run.timestamp).toLocaleString()"></td>
|
||||
<td class="px-4 py-3 text-sm" x-text="run.test_path || 'tests'"></td>
|
||||
<td class="px-4 py-3 text-center" x-text="run.total_tests"></td>
|
||||
<td class="px-4 py-3 text-center text-green-600" x-text="run.passed"></td>
|
||||
<td class="px-4 py-3 text-center text-red-600" x-text="run.failed + run.errors"></td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': run.pass_rate >= 90,
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': run.pass_rate >= 70 && run.pass_rate < 90,
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': run.pass_rate < 70
|
||||
}"
|
||||
x-text="run.pass_rate.toFixed(1) + '%'">
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDuration(run.duration_seconds)"></td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': run.status === 'passed',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': run.status === 'failed',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': run.status === 'running',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': run.status === 'error'
|
||||
}"
|
||||
x-text="run.status">
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Run Info -->
|
||||
<div x-show="stats.last_run" class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
Last run: <span x-text="stats.last_run ? new Date(stats.last_run).toLocaleString() : 'Never'"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
222
app/modules/dev_tools/templates/dev_tools/admin/testing-hub.html
Normal file
222
app/modules/dev_tools/templates/dev_tools/admin/testing-hub.html
Normal file
@@ -0,0 +1,222 @@
|
||||
{# app/templates/admin/testing-hub.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
|
||||
{% block title %}Testing Hub{% endblock %}
|
||||
|
||||
{# ✅ CRITICAL: Link to Alpine.js component #}
|
||||
{% block alpine_data %}adminTestingHub(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Testing Hub', back_url='/admin/dashboard', back_label='Back to Dashboard') }}
|
||||
|
||||
<!-- Introduction Card -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-lg shadow-lg p-6 mb-8">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('beaker', 'w-12 h-12')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-xl font-bold mb-2 text-gray-700 dark:text-gray-200">Testing & QA Tools</h3>
|
||||
<p class="text-gray-700 dark:text-gray-200 opacity-90">
|
||||
Comprehensive testing tools for manual QA, feature verification, and bug reproduction.
|
||||
These pages help you test specific flows without writing code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('clipboard-list', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Test Suites</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.totalSuites"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Test Cases</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.totalTests + '+'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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('lightning-bolt', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Features Covered</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.coverage"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
|
||||
<span x-html="$icon('clock', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Quick Tests</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.avgDuration"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Suites Grid -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<template x-for="suite in testSuites" :key="suite.id">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300">
|
||||
<div class="p-4 bg-gradient-to-r" :class="getColorClasses(suite.color).gradient">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xl font-semibold text-white flex items-center">
|
||||
<span x-html="$icon(suite.icon, 'w-6 h-6 mr-2 text-white')"></span>
|
||||
<span x-text="suite.name"></span>
|
||||
</h3>
|
||||
<span class="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-600 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-600">
|
||||
<span x-text="suite.testCount"></span> Tests
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4" x-text="suite.description"></p>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<template x-for="feature in suite.features" :key="feature">
|
||||
<div class="flex items-start text-sm">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 text-green-500 mr-2 mt-0.5 flex-shrink-0')"></span>
|
||||
<span class="text-gray-600 dark:text-gray-400" x-text="feature"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="goToTest(suite.url)"
|
||||
class="flex-1 flex items-center justify-center px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors"
|
||||
:class="getColorClasses(suite.color).button">
|
||||
<span x-html="$icon('play', 'w-4 h-4 mr-2 text-white')"></span>
|
||||
Run Tests
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Best Practices -->
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-800 dark:text-gray-200 flex items-center">
|
||||
<span x-html="$icon('light-bulb', 'w-6 h-6 mr-2 text-yellow-500')"></span>
|
||||
Testing Best Practices
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">Before Testing</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Ensure FastAPI server is running on localhost:8000</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Open browser DevTools (F12) to see console logs</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Check Network tab for API requests</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Clear localStorage before starting fresh tests</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">During Testing</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Follow test steps in order</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Check expected results against actual behavior</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Look for errors in console and network tabs</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Take screenshots if you find bugs</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Resources -->
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-800 dark:text-gray-200 flex items-center">
|
||||
<span x-html="$icon('book-open', 'w-5 h-5 mr-2 text-blue-600')"></span>
|
||||
Additional Resources
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">Component Library</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<a href="/admin/components">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>View all available UI components</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">Icons Browser</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<a href="/admin/icons">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>Browse all available icons</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">API Documentation</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start">
|
||||
<a href="/docs">
|
||||
<span class="text-purple-600 mr-2">•</span>
|
||||
<span>FastAPI endpoint reference</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{# ✅ CRITICAL: Load JavaScript file #}
|
||||
<script src="{{ url_for('dev_tools_static', path='admin/js/testing-hub.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user