refactor(js): migrate JavaScript files to module directories

Move 47 JS files from static/{admin,vendor,shared}/js/ to their
respective module directories app/modules/*/static/*/js/:

- Orders: orders.js, order-detail.js
- Catalog: products.js (renamed from vendor-products.js), product-*.js
- Inventory: inventory.js (admin & vendor)
- Customers: customers.js, users.js, user-*.js
- Billing: billing-history.js, subscriptions.js, subscription-tiers.js,
  billing.js, invoices.js, feature-store.js, upgrade-prompts.js
- Messaging: messages.js, notifications.js, email-templates.js
- Marketplace: marketplace*.js, letzshop*.js, onboarding.js
- Monitoring: monitoring.js, background-tasks.js, imports.js, logs.js
- Dev Tools: testing-*.js, code-quality-*.js

Update 39 templates to reference new module static paths using
url_for('{module}_static', path='...') pattern.

Files staying in static/ (platform core):
- admin: dashboard, login, platforms, vendors, companies, admin-users,
  settings, components, init-alpine, module-config
- vendor: dashboard, login, profile, settings, team, media, init-alpine
- shared: api-client, utils, money, icons, log-config, vendor-selector,
  media-picker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 22:08:20 +01:00
parent 434db1560a
commit 0b4291d893
86 changed files with 63 additions and 63 deletions

View File

@@ -0,0 +1,292 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
/**
* Code Quality Dashboard Component
* Manages the unified code quality dashboard page
* Supports multiple validator types: architecture, security, performance
*/
// Use centralized logger
const codeQualityLog = window.LogConfig.createLogger('CODE-QUALITY');
function codeQualityDashboard() {
return {
// Extend base data
...data(),
// Set current page for navigation
currentPage: 'code-quality',
// Validator type selection
selectedValidator: 'all', // 'all', 'architecture', 'security', 'performance'
validatorTypes: ['architecture', 'security', 'performance'],
// Dashboard-specific data
loading: false,
scanning: false,
scanDropdownOpen: false,
error: null,
successMessage: null,
scanProgress: null, // Progress message during scan
runningScans: [], // Track running scan IDs
pollInterval: null, // Polling interval ID
stats: {
total_violations: 0,
errors: 0,
warnings: 0,
info: 0,
open: 0,
assigned: 0,
resolved: 0,
ignored: 0,
technical_debt_score: 100,
trend: [],
by_severity: {},
by_rule: {},
by_module: {},
top_files: [],
last_scan: null,
validator_type: null,
by_validator: {}
},
async init() {
// Guard against multiple initialization
if (window._adminCodeQualityDashboardInitialized) return;
window._adminCodeQualityDashboardInitialized = true;
try {
// Check URL for validator_type parameter
const urlParams = new URLSearchParams(window.location.search);
const urlValidator = urlParams.get('validator_type');
if (urlValidator && this.validatorTypes.includes(urlValidator)) {
this.selectedValidator = urlValidator;
} else {
// Ensure 'all' is explicitly set as default
this.selectedValidator = 'all';
}
await this.loadStats();
// Check for any running scans on page load
await this.checkRunningScans();
} catch (error) {
codeQualityLog.error('Failed to initialize code quality dashboard:', error);
}
},
async checkRunningScans() {
try {
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
if (runningScans && runningScans.length > 0) {
this.scanning = true;
this.runningScans = runningScans.map(s => s.id);
this.updateProgressMessage(runningScans);
this.startPolling();
}
} catch (err) {
codeQualityLog.error('Failed to check running scans:', err);
}
},
updateProgressMessage(scans) {
const runningScans = scans.filter(s => s.status === 'running' || s.status === 'pending');
if (runningScans.length === 0) {
this.scanProgress = null;
return;
}
// Show progress from the first running scan
const firstRunning = runningScans.find(s => s.status === 'running');
if (firstRunning && firstRunning.progress_message) {
this.scanProgress = firstRunning.progress_message;
} else {
const validatorNames = runningScans.map(s => this.capitalizeFirst(s.validator_type));
this.scanProgress = `Running ${validatorNames.join(', ')} scan${runningScans.length > 1 ? 's' : ''}...`;
}
},
async loadStats() {
this.loading = true;
this.error = null;
try {
// Build URL with validator_type filter if not 'all'
let url = '/admin/code-quality/stats';
if (this.selectedValidator !== 'all') {
url += `?validator_type=${this.selectedValidator}`;
}
const stats = await apiClient.get(url);
this.stats = stats;
} catch (err) {
codeQualityLog.error('Failed to load stats:', err);
this.error = err.message;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.loading = false;
}
},
async selectValidator(validatorType) {
if (this.selectedValidator !== validatorType) {
this.selectedValidator = validatorType;
await this.loadStats();
// Update URL without reload
const url = new URL(window.location);
if (validatorType === 'all') {
url.searchParams.delete('validator_type');
} else {
url.searchParams.set('validator_type', validatorType);
}
window.history.pushState({}, '', url);
}
},
async runScan(validatorType = 'all') {
this.scanning = true;
this.error = null;
this.successMessage = null;
this.scanProgress = 'Queuing scan...';
try {
// Determine which validators to run
const validatorTypesToRun = validatorType === 'all'
? this.validatorTypes
: [validatorType];
const result = await apiClient.post('/admin/code-quality/scan', {
validator_types: validatorTypesToRun
});
// Store running scan IDs for polling
this.runningScans = result.scans.map(s => s.id);
// Show initial status message
const validatorNames = validatorTypesToRun.map(v => this.capitalizeFirst(v));
this.scanProgress = `Running ${validatorNames.join(', ')} scan${validatorNames.length > 1 ? 's' : ''}...`;
// Start polling for completion
this.startPolling();
} catch (err) {
codeQualityLog.error('Failed to run scan:', err);
this.error = err.message;
this.scanning = false;
this.scanProgress = null;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
}
},
startPolling() {
// Clear any existing polling
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
// Poll every 3 seconds
this.pollInterval = setInterval(async () => {
await this.pollScanStatus();
}, 3000);
},
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
},
async pollScanStatus() {
if (this.runningScans.length === 0) {
this.stopPolling();
this.scanning = false;
this.scanProgress = null;
return;
}
try {
const runningScans = await apiClient.get('/admin/code-quality/scans/running');
// Update progress message from running scans
this.updateProgressMessage(runningScans);
// Check if our scans have completed
const stillRunning = this.runningScans.filter(id =>
runningScans.some(s => s.id === id)
);
if (stillRunning.length === 0) {
// All scans completed - get final results
await this.handleScanCompletion();
} else {
// Update running scans list
this.runningScans = stillRunning;
}
} catch (err) {
codeQualityLog.error('Failed to poll scan status:', err);
}
},
async handleScanCompletion() {
this.stopPolling();
// Get results for all completed scans
let totalViolations = 0;
let totalErrors = 0;
let totalWarnings = 0;
const completedScans = [];
for (const scanId of this.runningScans) {
try {
const scan = await apiClient.get(`/admin/code-quality/scans/${scanId}/status`);
completedScans.push(scan);
totalViolations += scan.total_violations || 0;
totalErrors += scan.errors || 0;
totalWarnings += scan.warnings || 0;
} catch (err) {
codeQualityLog.error(`Failed to get scan ${scanId} results:`, err);
}
}
// Format success message based on number of validators run
if (completedScans.length > 1) {
this.successMessage = `Scan completed: ${totalViolations} total violations found (${totalErrors} errors, ${totalWarnings} warnings) across ${completedScans.length} validators`;
} else if (completedScans.length === 1) {
const scan = completedScans[0];
this.successMessage = `${this.capitalizeFirst(scan.validator_type)} scan completed: ${scan.total_violations} violations found (${scan.errors} errors, ${scan.warnings} warnings)`;
} else {
this.successMessage = 'Scan completed';
}
// Reload stats after scan
await this.loadStats();
// Reset scanning state
this.scanning = false;
this.scanProgress = null;
this.runningScans = [];
// Clear success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
},
async refresh() {
await this.loadStats();
},
capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
};
}

View File

@@ -0,0 +1,196 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
/**
* Code Quality Violations List Component
* Manages the violations list page with filtering and pagination
*/
// ✅ Use centralized logger
const codeQualityViolationsLog = window.LogConfig.createLogger('CODE-QUALITY');
function codeQualityViolations() {
return {
// Extend base data
...data(),
// Set current page for navigation
currentPage: 'code-quality',
// Violations-specific data
loading: false,
error: null,
violations: [],
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
filters: {
validator_type: '',
severity: '',
status: '',
rule_id: '',
file_path: ''
},
async init() {
// Guard against multiple initialization
if (window._codeQualityViolationsInitialized) {
codeQualityViolationsLog.warn('Already initialized, skipping');
return;
}
window._codeQualityViolationsInitialized = true;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// Load filters from URL params
const params = new URLSearchParams(window.location.search);
this.filters.validator_type = params.get('validator_type') || '';
this.filters.severity = params.get('severity') || '';
this.filters.status = params.get('status') || '';
this.filters.rule_id = params.get('rule_id') || '';
this.filters.file_path = params.get('file_path') || '';
await this.loadViolations();
},
async loadViolations() {
this.loading = true;
this.error = null;
try {
// Build query params
const params = {
page: this.pagination.page.toString(),
page_size: this.pagination.per_page.toString()
};
if (this.filters.validator_type) params.validator_type = this.filters.validator_type;
if (this.filters.severity) params.severity = this.filters.severity;
if (this.filters.status) params.status = this.filters.status;
if (this.filters.rule_id) params.rule_id = this.filters.rule_id;
if (this.filters.file_path) params.file_path = this.filters.file_path;
const data = await apiClient.get('/admin/code-quality/violations', params);
this.violations = data.violations;
this.pagination.total = data.total;
this.pagination.pages = data.total_pages;
// Update URL with current filters (without reloading)
this.updateURL();
} catch (err) {
codeQualityViolationsLog.error('Failed to load violations:', err);
this.error = err.message;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.loading = false;
}
},
applyFilters() {
// Reset to page 1 when filters change
this.pagination.page = 1;
this.loadViolations();
},
async nextPage() {
if (this.pagination.page < this.pagination.pages) {
this.pagination.page++;
await this.loadViolations();
}
},
// Computed: Total number of pages
get totalPages() {
return this.pagination.pages;
},
// Computed: Start index for pagination display
get startIndex() {
if (this.pagination.total === 0) return 0;
return (this.pagination.page - 1) * this.pagination.per_page + 1;
},
// Computed: End index for pagination display
get endIndex() {
const end = this.pagination.page * this.pagination.per_page;
return end > this.pagination.total ? this.pagination.total : end;
},
// Computed: Generate page numbers array with ellipsis
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.pagination.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current > 3) {
pages.push('...');
}
// Show pages around current page
const start = Math.max(2, current - 1);
const end = Math.min(totalPages - 1, current + 1);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (current < totalPages - 2) {
pages.push('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
},
async previousPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
await this.loadViolations();
}
},
goToPage(pageNum) {
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
this.pagination.page = pageNum;
this.loadViolations();
}
},
updateURL() {
const params = new URLSearchParams();
if (this.filters.validator_type) params.set('validator_type', this.filters.validator_type);
if (this.filters.severity) params.set('severity', this.filters.severity);
if (this.filters.status) params.set('status', this.filters.status);
if (this.filters.rule_id) params.set('rule_id', this.filters.rule_id);
if (this.filters.file_path) params.set('file_path', this.filters.file_path);
const newURL = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
window.history.replaceState({}, '', newURL);
}
};
}

View File

@@ -0,0 +1,249 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
/**
* Testing Dashboard Component
* Manages the pytest testing dashboard page
*/
// Use centralized logger
const testingDashboardLog = window.LogConfig.createLogger('TESTING-DASHBOARD');
function testingDashboard() {
return {
// Extend base data
...data(),
// Set current page for navigation
currentPage: 'testing',
// Dashboard-specific data
loading: false,
running: false,
collecting: false,
error: null,
successMessage: null,
activeRunId: null,
pollInterval: null,
elapsedTime: 0,
elapsedTimer: null,
// Statistics
stats: {
total_tests: 0,
passed: 0,
failed: 0,
errors: 0,
skipped: 0,
pass_rate: 0,
duration_seconds: 0,
coverage_percent: null,
last_run: null,
last_run_status: null,
total_test_files: 0,
collected_tests: 0,
unit_tests: 0,
integration_tests: 0,
performance_tests: 0,
system_tests: 0,
last_collected: null,
trend: [],
by_category: {},
top_failing: []
},
// Recent runs
runs: [],
async init() {
// Guard against multiple initialization
if (window._adminTestingDashboardInitialized) return;
window._adminTestingDashboardInitialized = true;
try {
testingDashboardLog.info('Initializing testing dashboard');
await this.loadStats();
await this.loadRuns();
// Check if there's a running test and resume polling
await this.checkForRunningTests();
} catch (error) {
testingDashboardLog.error('Failed to initialize testing dashboard:', error);
}
},
async checkForRunningTests() {
// Check if there's already a test running
const runningRun = this.runs.find(r => r.status === 'running');
if (runningRun) {
testingDashboardLog.info('Found running test:', runningRun.id);
this.running = true;
this.activeRunId = runningRun.id;
// Calculate elapsed time from when the run started
const startTime = new Date(runningRun.timestamp);
this.elapsedTime = Math.floor((Date.now() - startTime.getTime()) / 1000);
// Start elapsed time counter
this.elapsedTimer = setInterval(() => {
this.elapsedTime++;
}, 1000);
// Start polling for status
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
}
},
async loadStats() {
this.loading = true;
this.error = null;
try {
const stats = await apiClient.get('/admin/tests/stats');
this.stats = stats;
testingDashboardLog.info('Stats loaded:', stats);
} catch (err) {
testingDashboardLog.error('Failed to load stats:', err);
this.error = err.message;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.loading = false;
}
},
async loadRuns() {
try {
const runs = await apiClient.get('/admin/tests/runs?limit=10');
this.runs = runs;
testingDashboardLog.info('Runs loaded:', runs.length);
} catch (err) {
testingDashboardLog.error('Failed to load runs:', err);
// Don't set error - stats are more important
}
},
async runTests(testPath = 'tests') {
this.running = true;
this.error = null;
this.successMessage = null;
this.elapsedTime = 0;
testingDashboardLog.info('Starting tests:', testPath);
try {
// Start the test run (returns immediately)
const result = await apiClient.post('/admin/tests/run', {
test_path: testPath
});
testingDashboardLog.info('Test run started:', result);
this.activeRunId = result.id;
// Start elapsed time counter
this.elapsedTimer = setInterval(() => {
this.elapsedTime++;
}, 1000);
// Start polling for status
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
Utils.showToast('Test run started...', 'info');
} catch (err) {
testingDashboardLog.error('Failed to start tests:', err);
this.error = err.message;
this.running = false;
Utils.showToast('Failed to start tests: ' + err.message, 'error');
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
}
},
async pollRunStatus() {
if (!this.activeRunId) return;
try {
const run = await apiClient.get(`/admin/tests/runs/${this.activeRunId}`);
if (run.status !== 'running') {
// Test run completed
this.stopPolling();
testingDashboardLog.info('Test run completed:', run);
// Format success message
const status = run.status === 'passed' ? 'All tests passed!' : 'Tests completed with failures.';
this.successMessage = `${status} ${run.passed}/${run.total_tests} passed (${run.pass_rate.toFixed(1)}%) in ${this.formatDuration(run.duration_seconds)}`;
// Reload stats and runs
await this.loadStats();
await this.loadRuns();
// Show toast notification
Utils.showToast(this.successMessage, run.status === 'passed' ? 'success' : 'warning');
// Clear success message after 10 seconds
setTimeout(() => {
this.successMessage = null;
}, 10000);
}
} catch (err) {
testingDashboardLog.error('Failed to poll run status:', err);
// Don't stop polling on error, might be transient
}
},
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.elapsedTimer) {
clearInterval(this.elapsedTimer);
this.elapsedTimer = null;
}
this.running = false;
this.activeRunId = null;
},
async collectTests() {
this.collecting = true;
this.error = null;
testingDashboardLog.info('Collecting tests');
try {
const result = await apiClient.post('/admin/tests/collect');
testingDashboardLog.info('Collection completed:', result);
Utils.showToast(`Collected ${result.total_tests} tests from ${result.total_files} files`, 'success');
// Reload stats
await this.loadStats();
} catch (err) {
testingDashboardLog.error('Failed to collect tests:', err);
Utils.showToast('Failed to collect tests: ' + err.message, 'error');
} finally {
this.collecting = false;
}
},
async refresh() {
await this.loadStats();
await this.loadRuns();
},
formatDuration(seconds) {
if (seconds === null || seconds === undefined) return 'N/A';
if (seconds < 1) return `${Math.round(seconds * 1000)}ms`;
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const minutes = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${minutes}m ${secs}s`;
}
};
}

View File

@@ -0,0 +1,125 @@
// app/modules/dev_tools/static/admin/js/testing-hub.js
// ✅ Use centralized logger - ONE LINE!
// Create custom logger for testing hub
const testingLog = window.LogConfig.createLogger('TESTING-HUB');
/**
* Testing Hub Alpine.js Component
* Central hub for all test suites and QA tools
*/
function adminTestingHub() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'testing',
// Test suites data
testSuites: [
{
id: 'auth-flow',
name: 'Authentication Flow',
description: 'Test login, logout, token expiration, redirects, and protected route access.',
url: '/admin/test/auth-flow',
icon: 'lock-closed',
color: 'blue',
testCount: 6,
features: [
'Login with valid/invalid credentials',
'Token expiration handling',
'Protected route access & redirects',
'localStorage state monitoring'
]
},
{
id: 'vendors-users',
name: 'Data Migration & CRUD',
description: 'Test vendor and user creation, listing, editing, deletion, and data migration scenarios.',
url: '/admin/test/vendors-users-migration',
icon: 'database',
color: 'orange',
testCount: 10,
features: [
'Vendor CRUD operations',
'User management & roles',
'Data migration validation',
'Form validation & error handling'
]
}
],
// Stats
stats: {
totalSuites: 2,
totalTests: 16,
coverage: 'Auth, CRUD',
avgDuration: '< 5 min'
},
// Loading state
loading: false,
// ✅ CRITICAL: Proper initialization with guard
async init() {
testingLog.info('=== TESTING HUB INITIALIZING ===');
// Prevent multiple initializations
if (window._testingHubInitialized) {
testingLog.warn('Testing hub already initialized, skipping...');
return;
}
window._testingHubInitialized = true;
// Calculate stats
this.calculateStats();
testingLog.info('=== TESTING HUB INITIALIZATION COMPLETE ===');
},
/**
* Calculate test statistics
*/
calculateStats() {
this.stats.totalSuites = this.testSuites.length;
this.stats.totalTests = this.testSuites.reduce((sum, suite) => sum + suite.testCount, 0);
testingLog.debug('Stats calculated:', this.stats);
},
/**
* Get color classes for test suite cards
*/
getColorClasses(color) {
const colorMap = {
blue: {
gradient: 'from-blue-500 to-blue-600',
button: 'bg-blue-600 hover:bg-blue-700'
},
orange: {
gradient: 'from-orange-500 to-orange-600',
button: 'bg-orange-600 hover:bg-orange-700'
},
green: {
gradient: 'from-green-500 to-green-600',
button: 'bg-green-600 hover:bg-green-700'
},
purple: {
gradient: 'from-purple-500 to-purple-600',
button: 'bg-purple-600 hover:bg-purple-700'
}
};
return colorMap[color] || colorMap.blue;
},
/**
* Navigate to test suite
*/
goToTest(url) {
testingLog.info('Navigating to test suite:', url);
window.location.href = url;
}
};
}
testingLog.info('Testing hub module loaded');