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:
2026-02-01 14:34:16 +01:00
parent 843703258f
commit 4e28d91a78
542 changed files with 11603 additions and 9037 deletions

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

File diff suppressed because it is too large Load Diff

View 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=&quot;$icon(\'' + selectedIcon + '\', \'w-5 h-5\')&quot;'"></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>&lt;span x-html="$icon('home', 'w-5 h-5')"&gt;&lt;/span&gt;</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>&lt;span x-html="$icon('check', 'w-6 h-6 text-green-500')"&gt;&lt;/span&gt;</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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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 %}