adding developer tools in admin panel, adding vendor management

This commit is contained in:
2025-10-25 18:07:02 +02:00
parent 7bb69a9a96
commit 49890d4cbe
19 changed files with 3456 additions and 226 deletions

View File

@@ -0,0 +1,140 @@
// static/admin/js/components.js
// Setup logging
const COMPONENTS_LOG_LEVEL = 3;
const componentsLog = {
error: (...args) => COMPONENTS_LOG_LEVEL >= 1 && console.error('❌ [COMPONENTS ERROR]', ...args),
warn: (...args) => COMPONENTS_LOG_LEVEL >= 2 && console.warn('⚠️ [COMPONENTS WARN]', ...args),
info: (...args) => COMPONENTS_LOG_LEVEL >= 3 && console.info(' [COMPONENTS INFO]', ...args),
debug: (...args) => COMPONENTS_LOG_LEVEL >= 4 && console.log('🔍 [COMPONENTS DEBUG]', ...args)
};
/**
* Components Library Alpine.js Component
* UI components reference with live examples
*/
function adminComponents() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'components',
// Active section for navigation
activeSection: 'forms',
// Component sections
sections: [
{ id: 'forms', name: 'Forms', icon: 'clipboard-list' },
{ id: 'buttons', name: 'Buttons', icon: 'cursor-click' },
{ id: 'cards', name: 'Cards', icon: 'collection' },
{ id: 'badges', name: 'Badges', icon: 'tag' },
{ id: 'tables', name: 'Tables', icon: 'table' },
{ id: 'modals', name: 'Modals', icon: 'window' },
{ id: 'alerts', name: 'Alerts', icon: 'exclamation' }
],
// Sample form data for examples
exampleForm: {
textInput: 'Sample text',
email: 'user@example.com',
textarea: 'Sample description text...',
select: 'option1',
checkbox: true,
radio: 'option1',
disabled: 'Read-only value'
},
// Sample errors for validation examples
exampleErrors: {
email: 'Please enter a valid email address',
required: 'This field is required'
},
// ✅ CRITICAL: Proper initialization with guard
async init() {
componentsLog.info('=== COMPONENTS PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._componentsInitialized) {
componentsLog.warn('Components page already initialized, skipping...');
return;
}
window._componentsInitialized = true;
// Set active section from URL hash if present
this.setActiveSectionFromHash();
// Listen for hash changes
window.addEventListener('hashchange', () => {
this.setActiveSectionFromHash();
});
componentsLog.info('=== COMPONENTS PAGE INITIALIZATION COMPLETE ===');
},
/**
* Set active section from URL hash
*/
setActiveSectionFromHash() {
const hash = window.location.hash.replace('#', '');
if (hash && this.sections.find(s => s.id === hash)) {
this.activeSection = hash;
componentsLog.debug('Set active section from hash:', hash);
}
},
/**
* Navigate to section
*/
goToSection(sectionId) {
componentsLog.info('Navigating to section:', sectionId);
this.activeSection = sectionId;
window.location.hash = sectionId;
// Smooth scroll to section
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
},
/**
* Check if section is active
*/
isSectionActive(sectionId) {
return this.activeSection === sectionId;
},
/**
* Copy code to clipboard
*/
async copyCode(code) {
try {
await navigator.clipboard.writeText(code);
Utils.showToast('Code copied to clipboard!', 'success');
componentsLog.debug('Code copied to clipboard');
} catch (error) {
componentsLog.error('Failed to copy code:', error);
Utils.showToast('Failed to copy code', 'error');
}
},
/**
* Show toast example
*/
showToastExample(type) {
const messages = {
success: 'Operation completed successfully!',
error: 'An error occurred!',
warning: 'Please review your input.',
info: 'Here is some information.'
};
Utils.showToast(messages[type] || messages.info, type);
}
};
}
componentsLog.info('Components module loaded');

View File

@@ -0,0 +1,210 @@
// static/admin/js/icons-page.js
// Setup logging
const ICONS_PAGE_LOG_LEVEL = 3;
const iconsLog = {
error: (...args) => ICONS_PAGE_LOG_LEVEL >= 1 && console.error('❌ [ICONS PAGE ERROR]', ...args),
warn: (...args) => ICONS_PAGE_LOG_LEVEL >= 2 && console.warn('⚠️ [ICONS PAGE WARN]', ...args),
info: (...args) => ICONS_PAGE_LOG_LEVEL >= 3 && console.info(' [ICONS PAGE INFO]', ...args),
debug: (...args) => ICONS_PAGE_LOG_LEVEL >= 4 && console.log('🔍 [ICONS PAGE DEBUG]', ...args)
};
/**
* Icons Browser Alpine.js Component
* Browse and search all available icons
*/
function adminIcons() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'icons',
// Search and filter
searchQuery: '',
activeCategory: 'all',
// Icon categories
categories: [
{ id: 'all', name: 'All Icons', icon: 'collection' },
{ id: 'navigation', name: 'Navigation', icon: 'menu' },
{ id: 'user', name: 'User & Profile', icon: 'user' },
{ id: 'actions', name: 'Actions', icon: 'lightning-bolt' },
{ id: 'ecommerce', name: 'E-commerce', icon: 'shopping-bag' },
{ id: 'inventory', name: 'Inventory', icon: 'cube' },
{ id: 'communication', name: 'Communication', icon: 'mail' },
{ id: 'files', name: 'Files', icon: 'document' },
{ id: 'settings', name: 'Settings', icon: 'cog' },
{ id: 'status', name: 'Status', icon: 'check-circle' },
{ id: 'testing', name: 'Testing', icon: 'beaker' }
],
// All icons organized by category
iconsByCategory: {},
allIcons: [],
filteredIcons: [],
// Selected icon for detail view
selectedIcon: null,
// ✅ CRITICAL: Proper initialization with guard
async init() {
iconsLog.info('=== ICONS PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._iconsPageInitialized) {
iconsLog.warn('Icons page already initialized, skipping...');
return;
}
window._iconsPageInitialized = true;
// Load icons from global Icons object
this.loadIcons();
iconsLog.info('=== ICONS PAGE INITIALIZATION COMPLETE ===');
},
/**
* Load icons from global Icons object
*/
loadIcons() {
if (!window.Icons) {
iconsLog.error('Icons object not found! Make sure icons.js is loaded.');
return;
}
// Get all icon names
this.allIcons = Object.keys(window.Icons).map(name => ({
name: name,
category: this.categorizeIcon(name)
}));
// Organize by category
this.iconsByCategory = this.allIcons.reduce((acc, icon) => {
if (!acc[icon.category]) {
acc[icon.category] = [];
}
acc[icon.category].push(icon);
return acc;
}, {});
// Initial filter
this.filterIcons();
iconsLog.info(`Loaded ${this.allIcons.length} icons across ${Object.keys(this.iconsByCategory).length} categories`);
},
/**
* Categorize icon based on name
*/
categorizeIcon(iconName) {
const categoryMap = {
navigation: ['home', 'menu', 'search', 'arrow', 'chevron'],
user: ['user', 'identification', 'badge'],
actions: ['edit', 'delete', 'plus', 'check', 'close', 'refresh', 'duplicate', 'eye', 'filter', 'dots'],
ecommerce: ['shopping', 'credit-card', 'currency', 'gift', 'tag', 'truck', 'receipt'],
inventory: ['cube', 'collection', 'photograph', 'chart'],
communication: ['mail', 'phone', 'chat', 'bell', 'inbox'],
files: ['document', 'folder', 'download', 'upload'],
settings: ['cog', 'adjustments', 'calendar', 'moon', 'sun'],
status: ['exclamation', 'information', 'spinner', 'star', 'heart', 'flag'],
testing: ['view-grid', 'beaker', 'clipboard-list', 'check-circle', 'lightning-bolt', 'clock', 'lock-closed', 'database', 'light-bulb', 'book-open', 'play']
};
for (const [category, keywords] of Object.entries(categoryMap)) {
if (keywords.some(keyword => iconName.includes(keyword))) {
return category;
}
}
return 'navigation'; // default
},
/**
* Filter icons based on search and category
*/
filterIcons() {
let icons = this.allIcons;
// Filter by category
if (this.activeCategory !== 'all') {
icons = icons.filter(icon => icon.category === this.activeCategory);
}
// Filter by search query
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
icons = icons.filter(icon => icon.name.toLowerCase().includes(query));
}
this.filteredIcons = icons;
iconsLog.debug(`Filtered to ${icons.length} icons`);
},
/**
* Set active category
*/
setCategory(categoryId) {
iconsLog.info('Setting category:', categoryId);
this.activeCategory = categoryId;
this.filterIcons();
},
/**
* Select icon for detail view
*/
selectIcon(iconName) {
iconsLog.info('Selected icon:', iconName);
this.selectedIcon = iconName;
},
/**
* Copy icon usage code to clipboard
*/
async copyIconUsage(iconName) {
const code = `x-html="$icon('${iconName}', 'w-5 h-5')"`;
try {
await navigator.clipboard.writeText(code);
Utils.showToast(`'${iconName}' code copied!`, 'success');
iconsLog.debug('Icon usage code copied:', iconName);
} catch (error) {
iconsLog.error('Failed to copy code:', error);
Utils.showToast('Failed to copy code', 'error');
}
},
/**
* Copy icon name to clipboard
*/
async copyIconName(iconName) {
try {
await navigator.clipboard.writeText(iconName);
Utils.showToast(`'${iconName}' copied!`, 'success');
iconsLog.debug('Icon name copied:', iconName);
} catch (error) {
iconsLog.error('Failed to copy name:', error);
Utils.showToast('Failed to copy name', 'error');
}
},
/**
* Get category info
*/
getCategoryInfo(categoryId) {
return this.categories.find(c => c.id === categoryId) || this.categories[0];
},
/**
* Get icon count for category
*/
getCategoryCount(categoryId) {
if (categoryId === 'all') {
return this.allIcons.length;
}
return this.iconsByCategory[categoryId]?.length || 0;
}
};
}
iconsLog.info('Icons page module loaded');

View File

@@ -0,0 +1,131 @@
// static/admin/js/testing-hub.js
// Setup logging
const TESTING_HUB_LOG_LEVEL = 3;
const testingLog = {
error: (...args) => TESTING_HUB_LOG_LEVEL >= 1 && console.error('❌ [TESTING HUB ERROR]', ...args),
warn: (...args) => TESTING_HUB_LOG_LEVEL >= 2 && console.warn('⚠️ [TESTING HUB WARN]', ...args),
info: (...args) => TESTING_HUB_LOG_LEVEL >= 3 && console.info(' [TESTING HUB INFO]', ...args),
debug: (...args) => TESTING_HUB_LOG_LEVEL >= 4 && console.log('🔍 [TESTING HUB DEBUG]', ...args)
};
/**
* 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');

View File

@@ -0,0 +1,135 @@
// static/admin/js/vendor-detail.js
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
const VENDOR_DETAIL_LOG_LEVEL = 3;
const detailLog = {
error: (...args) => VENDOR_DETAIL_LOG_LEVEL >= 1 && console.error('❌ [VENDOR_DETAIL ERROR]', ...args),
warn: (...args) => VENDOR_DETAIL_LOG_LEVEL >= 2 && console.warn('⚠️ [VENDOR_DETAIL WARN]', ...args),
info: (...args) => VENDOR_DETAIL_LOG_LEVEL >= 3 && console.info(' [VENDOR_DETAIL INFO]', ...args),
debug: (...args) => VENDOR_DETAIL_LOG_LEVEL >= 4 && console.log('🔍 [VENDOR_DETAIL DEBUG]', ...args)
};
function adminVendorDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Vendor detail page specific state
currentPage: 'vendor-detail',
vendor: null,
loading: false,
error: null,
vendorCode: null,
// Initialize
async init() {
detailLog.info('=== VENDOR DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._vendorDetailInitialized) {
detailLog.warn('Vendor detail page already initialized, skipping...');
return;
}
window._vendorDetailInitialized = true;
// Get vendor code from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/vendors\/([^\/]+)$/);
if (match) {
this.vendorCode = match[1];
detailLog.info('Viewing vendor:', this.vendorCode);
await this.loadVendor();
} else {
detailLog.error('No vendor code in URL');
this.error = 'Invalid vendor URL';
Utils.showToast('Invalid vendor URL', 'error');
}
detailLog.info('=== VENDOR DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load vendor data
async loadVendor() {
detailLog.info('Loading vendor details...');
this.loading = true;
this.error = null;
try {
const startTime = Date.now();
const response = await apiClient.get(`/admin/vendors/${this.vendorCode}`);
const duration = Date.now() - startTime;
this.vendor = response;
detailLog.info(`Vendor loaded in ${duration}ms`, {
vendor_code: this.vendor.vendor_code,
name: this.vendor.name,
is_verified: this.vendor.is_verified,
is_active: this.vendor.is_active
});
detailLog.debug('Full vendor data:', this.vendor);
} catch (error) {
detailLog.error('Failed to load vendor:', error);
this.error = error.message || 'Failed to load vendor details';
Utils.showToast('Failed to load vendor details', 'error');
} finally {
this.loading = false;
}
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
detailLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
detailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// Delete vendor
async deleteVendor() {
detailLog.info('Delete vendor requested:', this.vendorCode);
if (!confirm(`Are you sure you want to delete vendor "${this.vendor.name}"?\n\nThis action cannot be undone and will delete:\n- All products\n- All orders\n- All customers\n- All team members`)) {
detailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nType the vendor code to confirm: ${this.vendor.vendor_code}\n\nAre you absolutely sure?`)) {
detailLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
detailLog.info('Deleting vendor:', this.vendorCode);
await apiClient.delete(`/admin/vendors/${this.vendorCode}?confirm=true`);
Utils.showToast('Vendor deleted successfully', 'success');
detailLog.info('Vendor deleted successfully');
// Redirect to vendors list
setTimeout(() => window.location.href = '/admin/vendors', 1500);
} catch (error) {
detailLog.error('Failed to delete vendor:', error);
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
}
},
// Refresh vendor data
async refresh() {
detailLog.info('=== VENDOR REFRESH TRIGGERED ===');
await this.loadVendor();
Utils.showToast('Vendor details refreshed', 'success');
detailLog.info('=== VENDOR REFRESH COMPLETE ===');
}
};
}
detailLog.info('Vendor detail module loaded');

View File

@@ -18,8 +18,10 @@ function adminVendors() {
// Inherit base layout functionality from init-alpine.js
...data(),
// Vendors page specific state
// ✅ CRITICAL: Page identifier for sidebar active state
currentPage: 'vendors',
// Vendors page specific state
vendors: [],
stats: {
total: 0,
@@ -30,8 +32,8 @@ function adminVendors() {
loading: false,
error: null,
// Pagination state
currentPage: 1,
// Pagination state (renamed from currentPage to avoid conflict)
page: 1, // ✅ FIXED: Was 'currentPage' which conflicted with sidebar
itemsPerPage: 10,
// Initialize
@@ -53,7 +55,7 @@ function adminVendors() {
// Computed: Get paginated vendors for current page
get paginatedVendors() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const start = (this.page - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.vendors.slice(start, end);
},
@@ -66,12 +68,12 @@ function adminVendors() {
// Computed: Start index for pagination display
get startIndex() {
if (this.vendors.length === 0) return 0;
return (this.currentPage - 1) * this.itemsPerPage + 1;
return (this.page - 1) * this.itemsPerPage + 1;
},
// Computed: End index for pagination display
get endIndex() {
const end = this.currentPage * this.itemsPerPage;
const end = this.page * this.itemsPerPage;
return end > this.vendors.length ? this.vendors.length : end;
},
@@ -79,7 +81,7 @@ function adminVendors() {
get pageNumbers() {
const pages = [];
const totalPages = this.totalPages;
const current = this.currentPage;
const current = this.page;
if (totalPages <= 7) {
// Show all pages if 7 or fewer
@@ -137,7 +139,7 @@ function adminVendors() {
}
// Reset to first page when data is loaded
this.currentPage = 1;
this.page = 1;
} catch (error) {
vendorsLog.error('Failed to load vendors:', error);
@@ -167,27 +169,27 @@ function adminVendors() {
},
// Pagination: Go to specific page
goToPage(page) {
if (page === '...' || page < 1 || page > this.totalPages) {
goToPage(pageNum) {
if (pageNum === '...' || pageNum < 1 || pageNum > this.totalPages) {
return;
}
vendorsLog.info('Going to page:', page);
this.currentPage = page;
vendorsLog.info('Going to page:', pageNum);
this.page = pageNum;
},
// Pagination: Go to next page
nextPage() {
if (this.currentPage < this.totalPages) {
if (this.page < this.totalPages) {
vendorsLog.info('Going to next page');
this.currentPage++;
this.page++;
}
},
// Pagination: Go to previous page
previousPage() {
if (this.currentPage > 1) {
if (this.page > 1) {
vendorsLog.info('Going to previous page');
this.currentPage--;
this.page--;
}
},

View File

@@ -1,644 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Flow Testing - Admin Panel</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 20px;
background: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 6px;
border-left: 4px solid #3b82f6;
}
.test-section h2 {
color: #333;
margin-bottom: 15px;
font-size: 20px;
}
.test-description {
color: #666;
margin-bottom: 15px;
font-size: 14px;
}
.test-steps {
background: white;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.test-steps ol {
margin-left: 20px;
}
.test-steps li {
margin-bottom: 8px;
color: #444;
}
.expected-result {
background: #e8f5e9;
padding: 12px;
border-radius: 4px;
border-left: 3px solid #4caf50;
margin-bottom: 15px;
}
.expected-result strong {
color: #2e7d32;
display: block;
margin-bottom: 5px;
}
.expected-result ul {
margin-left: 20px;
color: #555;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
button:active {
transform: translateY(0);
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-warning:hover {
background: #d97706;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.status-panel {
background: #1e293b;
color: #e2e8f0;
padding: 20px;
border-radius: 6px;
margin-top: 30px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.status-panel h3 {
color: #38bdf8;
margin-bottom: 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.status-item {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
}
.status-label {
color: #94a3b8;
}
.status-value {
color: #34d399;
font-weight: 500;
}
.status-value.false {
color: #f87171;
}
.log-level-control {
background: #fef3c7;
padding: 15px;
border-radius: 6px;
margin-bottom: 30px;
border-left: 4px solid #f59e0b;
}
.log-level-control h3 {
color: #92400e;
margin-bottom: 10px;
font-size: 16px;
}
.log-level-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.log-level-buttons button {
padding: 8px 16px;
font-size: 12px;
}
.warning-box {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
padding: 15px;
margin-top: 30px;
}
.warning-box h3 {
color: #991b1b;
margin-bottom: 10px;
font-size: 16px;
}
.warning-box ul {
margin-left: 20px;
color: #7f1d1d;
}
.warning-box li {
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 Auth Flow Testing</h1>
<p class="subtitle">Comprehensive testing for the Jinja2 migration auth loop fix</p>
<!-- Log Level Control -->
<div class="log-level-control">
<h3>📊 Log Level Control</h3>
<p style="color: #78350f; font-size: 13px; margin-bottom: 10px;">
Change logging verbosity for login.js and api-client.js
</p>
<div class="log-level-buttons">
<button onclick="setLogLevel(0)" class="btn-secondary">0 - None</button>
<button onclick="setLogLevel(1)" class="btn-danger">1 - Errors Only</button>
<button onclick="setLogLevel(2)" class="btn-warning">2 - Warnings</button>
<button onclick="setLogLevel(3)" class="btn-success">3 - Info (Production)</button>
<button onclick="setLogLevel(4)" class="btn-primary">4 - Debug (Full)</button>
</div>
<p style="color: #78350f; font-size: 12px; margin-top: 10px; font-style: italic;">
Current levels: LOGIN = <span id="currentLoginLevel">4</span>, API = <span id="currentApiLevel">3</span>
</p>
</div>
<!-- Test 1: Clean Slate -->
<div class="test-section">
<h2>Test 1: Clean Slate - Fresh Login Flow</h2>
<p class="test-description">
Tests the complete login flow from scratch with no existing tokens.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Clear All Data" below</li>
<li>Click "Navigate to /admin"</li>
<li>Observe browser behavior and console logs</li>
<li>You should land on login page</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Single redirect: /admin → /admin/login</li>
<li>Login page loads with NO API calls to /admin/auth/me</li>
<li>No loops, no errors in console</li>
<li>Form is ready for input</li>
</ul>
</div>
<div class="button-group">
<button onclick="clearAllData()" class="btn-danger">Clear All Data</button>
<button onclick="navigateToAdmin()" class="btn-primary">Navigate to /admin</button>
<button onclick="navigateToLogin()" class="btn-secondary">Go to Login</button>
</div>
</div>
<!-- Test 2: Login Success -->
<div class="test-section">
<h2>Test 2: Successful Login</h2>
<p class="test-description">
Tests that login works correctly and redirects to dashboard.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Ensure you're on /admin/login</li>
<li>Enter valid admin credentials</li>
<li>Click "Login"</li>
<li>Observe redirect and dashboard load</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Login API call succeeds (check Network tab)</li>
<li>Token stored in localStorage</li>
<li>Success message shows briefly</li>
<li>Redirect to /admin/dashboard after 500ms</li>
<li>Dashboard loads with stats and recent vendors</li>
</ul>
</div>
<div class="button-group">
<button onclick="navigateToLogin()" class="btn-primary">Go to Login Page</button>
<button onclick="checkAuthStatus()" class="btn-secondary">Check Auth Status</button>
</div>
</div>
<!-- Test 3: Dashboard Refresh -->
<div class="test-section">
<h2>Test 3: Dashboard Refresh (Authenticated)</h2>
<p class="test-description">
Tests that refreshing the dashboard works without redirect loops.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Complete Test 2 (login successfully)</li>
<li>On dashboard, press F5 or click "Refresh Page"</li>
<li>Observe page reload behavior</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Dashboard reloads normally</li>
<li>No redirects to login</li>
<li>Stats and vendors load correctly</li>
<li>No console errors</li>
</ul>
</div>
<div class="button-group">
<button onclick="navigateToDashboard()" class="btn-primary">Go to Dashboard</button>
<button onclick="window.location.reload()" class="btn-secondary">Refresh Page</button>
</div>
</div>
<!-- Test 4: Expired Token -->
<div class="test-section">
<h2>Test 4: Expired Token Handling</h2>
<p class="test-description">
Tests that expired tokens are handled gracefully with redirect to login.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Set Expired Token"</li>
<li>Click "Navigate to Dashboard"</li>
<li>Observe authentication failure and redirect</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Server detects expired token</li>
<li>Returns 401 Unauthorized</li>
<li>Browser redirects to /admin/login</li>
<li>Token is cleared from localStorage</li>
<li>No infinite loops</li>
</ul>
</div>
<div class="button-group">
<button onclick="setExpiredToken()" class="btn-warning">Set Expired Token</button>
<button onclick="navigateToDashboard()" class="btn-primary">Navigate to Dashboard</button>
</div>
</div>
<!-- Test 5: Direct Dashboard Access (No Token) -->
<div class="test-section">
<h2>Test 5: Direct Dashboard Access (Unauthenticated)</h2>
<p class="test-description">
Tests that accessing dashboard without token redirects to login.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Clear All Data"</li>
<li>Click "Navigate to Dashboard"</li>
<li>Observe immediate redirect to login</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Redirect from /admin/dashboard to /admin/login</li>
<li>No API calls attempted</li>
<li>Login page loads correctly</li>
</ul>
</div>
<div class="button-group">
<button onclick="clearAllData()" class="btn-danger">Clear All Data</button>
<button onclick="navigateToDashboard()" class="btn-primary">Navigate to Dashboard</button>
</div>
</div>
<!-- Test 6: Login Page with Valid Token -->
<div class="test-section">
<h2>Test 6: Login Page with Valid Token</h2>
<p class="test-description">
Tests what happens when user visits login page while already authenticated.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Login successfully (Test 2)</li>
<li>Click "Go to Login Page" below</li>
<li>Observe behavior</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Login page loads</li>
<li>Existing token is cleared (init() clears it)</li>
<li>Form is displayed normally</li>
<li>NO redirect loops</li>
<li>NO API calls to validate token</li>
</ul>
</div>
<div class="button-group">
<button onclick="setValidToken()" class="btn-success">Set Valid Token (Mock)</button>
<button onclick="navigateToLogin()" class="btn-primary">Go to Login Page</button>
</div>
</div>
<!-- Status Panel -->
<div class="status-panel">
<h3>🔍 Current Auth Status</h3>
<div id="statusDisplay">
<div class="status-item">
<span class="status-label">Current URL:</span>
<span class="status-value" id="currentUrl">-</span>
</div>
<div class="status-item">
<span class="status-label">Has admin_token:</span>
<span class="status-value" id="hasToken">-</span>
</div>
<div class="status-item">
<span class="status-label">Has admin_user:</span>
<span class="status-value" id="hasUser">-</span>
</div>
<div class="status-item">
<span class="status-label">Token Preview:</span>
<span class="status-value" id="tokenPreview">-</span>
</div>
<div class="status-item">
<span class="status-label">Username:</span>
<span class="status-value" id="username">-</span>
</div>
</div>
<button onclick="updateStatus()" style="margin-top: 15px; background: #38bdf8; color: #0f172a; padding: 8px 16px; border-radius: 4px; font-size: 12px; cursor: pointer; border: none;">
🔄 Refresh Status
</button>
</div>
<!-- Warning Box -->
<div class="warning-box">
<h3>⚠️ Important Notes</h3>
<ul>
<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>
<script>
// Update status display
function 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);
}
document.getElementById('currentUrl').textContent = window.location.href;
const hasTokenEl = document.getElementById('hasToken');
hasTokenEl.textContent = token ? 'Yes' : 'No';
hasTokenEl.className = token ? 'status-value' : 'status-value false';
const hasUserEl = document.getElementById('hasUser');
hasUserEl.textContent = user ? 'Yes' : 'No';
hasUserEl.className = user ? 'status-value' : 'status-value false';
document.getElementById('tokenPreview').textContent = token
? token.substring(0, 30) + '...'
: 'No token';
document.getElementById('username').textContent = user?.username || 'Not logged in';
console.log('📊 Status Updated:', {
hasToken: !!token,
hasUser: !!user,
user: user
});
}
// Test functions
function clearAllData() {
console.log('🗑️ Clearing all localStorage data...');
localStorage.clear();
console.log('✅ All data cleared');
alert('✅ All localStorage data cleared!\n\nCheck console for details.');
updateStatus();
}
function navigateToAdmin() {
console.log('🚀 Navigating to /admin...');
window.location.href = '/admin';
}
function navigateToLogin() {
console.log('🚀 Navigating to /admin/login...');
window.location.href = '/admin/login';
}
function navigateToDashboard() {
console.log('🚀 Navigating to /admin/dashboard...');
window.location.href = '/admin/dashboard';
}
function checkAuthStatus() {
updateStatus();
alert('Check console and status panel for auth details.');
}
function setExpiredToken() {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.invalidexpiredtoken';
console.log('⚠️ Setting expired/invalid token...');
localStorage.setItem('admin_token', expiredToken);
localStorage.setItem('admin_user', JSON.stringify({
id: 1,
username: 'test_expired',
role: 'admin'
}));
console.log('✅ Expired token set');
alert('⚠️ Expired token set!\n\nNow try navigating to dashboard.');
updateStatus();
}
function setValidToken() {
// This is a mock token - won't actually work with backend
const mockToken = 'mock_valid_token_' + Date.now();
console.log('✅ Setting mock valid token...');
localStorage.setItem('admin_token', mockToken);
localStorage.setItem('admin_user', JSON.stringify({
id: 1,
username: 'test_user',
role: 'admin'
}));
console.log('✅ Mock token set (will not work with real backend)');
alert('✅ Mock token set!\n\nNote: This is a fake token and won\'t work with the real backend.');
updateStatus();
}
// Log level control
function setLogLevel(level) {
console.log(`📊 Setting log level to ${level}...`);
// Note: This only works if login.js and api-client.js are loaded
// In production, you'd need to reload the page or use a more sophisticated approach
if (typeof LOG_LEVEL !== 'undefined') {
window.LOG_LEVEL = level;
document.getElementById('currentLoginLevel').textContent = level;
console.log('✅ LOGIN log level set to', level);
} else {
console.warn('⚠️ LOG_LEVEL not found (login.js not loaded)');
}
if (typeof API_LOG_LEVEL !== 'undefined') {
window.API_LOG_LEVEL = level;
document.getElementById('currentApiLevel').textContent = level;
console.log('✅ API log level set to', level);
} else {
console.warn('⚠️ API_LOG_LEVEL not found (api-client.js not loaded)');
}
alert(`Log level set to ${level}\n\n0 = None\n1 = Errors\n2 = Warnings\n3 = Info\n4 = Debug\n\nNote: Changes apply to current page. Reload to apply to all scripts.`);
}
// Initialize status on load
updateStatus();
// Auto-refresh status every 2 seconds
setInterval(updateStatus, 2000);
console.log('🧪 Auth Flow Testing Script Loaded');
console.log('📊 Use the buttons above to run tests');
console.log('🔍 Watch browser console and Network tab for details');
</script>
</body>
</html>