Fix user display, add logout, footer, optimize performance

- Move userDisplay inside tableContainer with proper positioning
- Add logout button next to settings
- Add footer with user email (left) and version (right)
- Fix auth model loading on page refresh
- Remove cache clearing on checkbox changes for better performance
- Add 150ms debounce to search input
- Expand search to include Notes fields
- Suppress QUIC protocol console errors from realtime connection
- Optimize checkbox change handler
This commit is contained in:
2026-01-02 02:07:33 +00:00
parent 2365bb9b4d
commit 5a34170454
4 changed files with 96 additions and 59 deletions
-20
View File
@@ -1,20 +0,0 @@
# Azure AD / Microsoft Authentication
CLIENT_ID=your-client-id
CLIENT_SECRET=your-client-secret
TENANT_ID=your-tenant-id
REDIRECT_URI=https://your-domain.com/auth/callback
# PocketBase Configuration
PB_DB=https://pocketbase.ccllc.pro
PB_COLLECTION=Job_Info_TestEnv
PB_AUTH_COLLECTION=Users
# Redis/Valkey Configuration (Caching & Search)
# Connect to the same Redis/Valkey instance as Job-Info-Prod
REDIS_URL=redis://localhost:6379
# For production, use the same connection string as Job-Info-Prod
# Example: REDIS_URL=redis://redis-server:6379
# Example with auth: REDIS_URL=redis://:password@redis-server:6379
# Optional
PORT=3025
+2
View File
@@ -250,6 +250,8 @@
console.log('Settings saved to PocketBase successfully');
if (!isAuto) {
alert('Settings saved successfully!');
// Redirect back to estimator table
window.location.href = '/estimatortable.html';
}
} catch (error) {
console.error('Error saving settings:', error);
+91 -36
View File
@@ -26,6 +26,7 @@
/* Container layout - keep header stationary */
#tableContainer {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
@@ -168,17 +169,6 @@
</head>
<body class="h-screen overflow-hidden bg-gradient-to-br from-primary to-secondary font-sans">
<!-- Top-right display name and settings -->
<div id="userDisplay" class="hidden absolute top-4 right-4 flex items-center space-x-3">
<span id="userDisplayName" class="text-white text-sm font-semibold drop-shadow"></span>
<button id="settingsBtn" class="text-white hover:text-gray-200 transition-colors" title="Settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
<!-- Login Container -->
<div id="loginContainer" class="hidden max-w-md mx-auto bg-white rounded-2xl shadow-2xl p-8 mt-20">
<div class="text-center mb-8">
@@ -202,6 +192,22 @@
<!-- Table Container -->
<div id="tableContainer" class="hidden">
<!-- Top-right display name and settings -->
<div id="userDisplay" class="hidden absolute top-4 right-4 flex items-center space-x-3" style="z-index: 1000;">
<span id="userDisplayName" class="text-white text-sm font-semibold drop-shadow"></span>
<button id="settingsBtn" class="text-white hover:text-gray-200 transition-colors" title="Settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<button id="logoutBtn" class="text-white hover:text-red-300 transition-colors" title="Logout">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
<!-- Header Section -->
<div class="header-section bg-white rounded-lg shadow-2xl p-6">
<h1 class="text-3xl font-bold text-gray-800">Estimator Job Table</h1>
@@ -288,10 +294,26 @@
</table>
</div>
</div>
<!-- Footer -->
<div class="mt-2 flex justify-between items-center text-white text-xs px-4">
<span id="footerEmail"></span>
<span>v1.0.0-alpha4</span>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
<script>
// Suppress QUIC protocol errors (cosmetic browser issue with PocketBase realtime)
const originalConsoleError = console.error;
console.error = function(...args) {
const message = args.join(' ');
if (message.includes('ERR_QUIC_PROTOCOL_ERROR') || message.includes('/api/realtime')) {
return; // Suppress these specific errors
}
originalConsoleError.apply(console, args);
};
// Initialize PocketBase client
const pb = new PocketBase('https://pocketbase.ccllc.pro');
@@ -333,7 +355,6 @@
const safeOrder = Number.isFinite(orderNum) ? orderNum : idx + 1;
return { ...col, order: safeOrder };
}).sort((a, b) => a.order - b.order);
console.log('Normalized columns (first 5):', normalized.slice(0, 5).map(c => ({ name: c.name, order: c.order, visible: c.visible })));
return normalized;
}
@@ -356,7 +377,6 @@
}
tableReloadTimer = setTimeout(() => {
if (isLoadingTableData) {
console.log('Table data already loading, skipping reload');
return;
}
loadTableData();
@@ -366,13 +386,10 @@
// Load app settings from PocketBase (global settings)
async function loadAppSettings() {
try {
console.log('Loading app settings for:', APP_NAME);
const records = await pb.collection('app_preferences_settings').getFullList({
filter: `app_name = "${APP_NAME}"`
});
console.log('Settings records found:', records.length);
if (records.length > 0) {
const settingsData = records[0].app_json_prefs;
appSettingsRecordId = records[0].id;
@@ -565,6 +582,17 @@
console.log('User authenticated, loading...');
loadingIndicator.classList.remove('hidden');
// If model is missing, try to get current user
if (!pb.authStore.model) {
try {
const user = await pb.collection('users').getOne(pb.authStore.token.split('.')[1] ? JSON.parse(atob(pb.authStore.token.split('.')[1])).id : '');
pb.authStore.save(pb.authStore.token, user);
console.log('Loaded user model:', user);
} catch (err) {
console.error('Failed to load user model:', err);
}
}
await loadAppSettings();
console.log('Settings loaded');
@@ -592,7 +620,15 @@
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
console.log('Login successful');
console.log('Login successful, authData:', authData);
console.log('Auth record:', authData.record);
// Ensure the model is properly saved
if (authData.record) {
pb.authStore.save(pb.authStore.token, authData.record);
console.log('Saved user model to authStore:', authData.record);
}
await loadAppSettings();
await startSettingsWatch();
showTable();
@@ -610,9 +646,25 @@
loginContainer.classList.add('hidden');
tableContainer.classList.remove('hidden');
console.log('showTable() called - pb.authStore:', pb.authStore);
console.log('showTable() - pb.authStore.model:', pb.authStore.model);
console.log('showTable() - pb.authStore.isValid:', pb.authStore.isValid);
if (pb.authStore.model) {
const displayName = pb.authStore.model.name || pb.authStore.model.email || 'User';
const email = pb.authStore.model.email || '';
console.log('USER INFO - Display Name:', displayName, '| Email:', email);
userDisplay.classList.remove('hidden');
userDisplayName.textContent = pb.authStore.model.name || pb.authStore.model.email || 'User';
userDisplayName.textContent = displayName;
// Set footer email
const footerEmail = document.getElementById('footerEmail');
if (footerEmail) {
footerEmail.textContent = email;
}
} else {
console.log('ERROR: pb.authStore.model is null/undefined!');
}
// Setup search listener after table is shown
@@ -624,6 +676,11 @@
if (e.target.closest('#settingsBtn')) {
window.location.href = '/appsettings.html';
}
if (e.target.closest('#logoutBtn')) {
pb.authStore.clear();
window.location.reload();
}
});
// Format date from UTC to Central Time (MM/dd/yyyy)
@@ -964,7 +1021,10 @@
'Office_Rep',
'Project_Manager',
'Company_Client',
'Contact_Person'
'Contact_Person',
'Notes',
'latest_note_summary',
'note_thread_text'
];
return records.filter(record => {
@@ -981,9 +1041,18 @@
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
let searchTimeout;
searchInput.addEventListener('input', (e) => {
currentSearchTerm = e.target.value;
renderTableRows();
// Clear existing timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Debounce search to avoid re-rendering on every keystroke
searchTimeout = setTimeout(() => {
currentSearchTerm = e.target.value;
renderTableRows();
}, 150); // 150ms debounce - feels instant but avoids excessive renders
});
}
@@ -1010,21 +1079,7 @@
await pb.collection('Job_Info_TestEnv').update(recordId, {
[fieldName]: newValue
});
console.log(`Updated ${fieldName} to ${newValue} for record ${recordId}`);
// Clear cache to force fresh data on next load
try {
await fetch('/api/cache/clear', {
method: 'POST',
headers: {
'X-PocketBase-Token': pb.authStore.token
}
});
console.log('Cache cleared after update');
} catch (cacheError) {
console.warn('Failed to clear cache:', cacheError);
// Non-fatal - data is still updated in PocketBase
}
// Note: Cache will auto-expire in 5 minutes, no need to clear immediately
} catch (error) {
console.error('Error updating field:', error);
// Revert checkbox on error
+3 -3
View File
@@ -113,12 +113,12 @@ app.post('/api/admin/login', async (c) => {
return c.json({ message: 'Missing email or password' }, 400);
}
// Authenticate as PocketBase superuser via `_superusers` collection
const authData = await pb.collection('_superusers').authWithPassword(email, password);
// Authenticate as regular user
const authData = await pb.collection('users').authWithPassword(email, password);
if (!authData || !authData.token) {
return c.json({ message: 'Invalid credentials' }, 401);
}
return c.json({ ok: true, token: authData.token, email: authData.record?.email, role: 'superuser' });
return c.json({ ok: true, token: authData.token, email: authData.record?.email });
} catch (err) {
console.error('admin_login_error', { message: (err as Error)?.message });
return c.json({ message: 'Login failed' }, 401);