Swap the muddy #f3ae3e palette for the real OLIVER brand pulled from the master
PPT template: yellow #FFCB05 + near-black #1A1A1A + off-white #F6F7F7, Montserrat
font. White-first page with a brand-yellow highlight rectangle behind page titles,
stat tiles with yellow left-strip, and a short yellow accent line under each
card section title — picks up the template's "01" chapter-marker rhythm.
Fixes two production bugs along the way:
- Nav stays pinned at top while page scrolls. The conflicting
`.navbar { position: relative !important }` rule was removed from nav.html
so the `position: fixed` from style.css can take effect.
- Clicking admin tabs no longer scrolls the page. Converted
`<a href="#users">` to `<button data-bs-target="#users">` (Bootstrap 5's
recommended pattern), so the anchor jump can't happen.
Other refinements: table padding loosened, `transform: scale` row hover
removed (jittery on dense rows), modal headers switched to near-black,
Chart.js palette aligned with brand tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
469 lines
No EOL
15 KiB
HTML
469 lines
No EOL
15 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}My Profile - AgentHub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container my-5">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2 class="page-title">My Profile</h2>
|
|
<p class="page-subtitle">Manage your account information and activity</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Profile Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0"><i class="fas fa-user-circle me-2"></i>Profile Information</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-2 text-center mb-3">
|
|
<div class="profile-avatar">
|
|
{{ current_user.full_name[0] if current_user.full_name else current_user.email[0] }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-10">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-item">
|
|
<span class="info-label">Full Name</span>
|
|
<span class="info-value">{{ current_user.full_name or 'Not set' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-item">
|
|
<span class="info-label">Email Address</span>
|
|
<span class="info-value">{{ current_user.email }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-item">
|
|
<span class="info-label">Account Type</span>
|
|
<span class="info-value">
|
|
{% if current_user.is_admin %}
|
|
<span class="badge bg-danger">Administrator</span>
|
|
{% else %}
|
|
<span class="badge bg-primary">User</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-item">
|
|
<span class="info-label">Member Since</span>
|
|
<span class="info-value">{{ current_user.created_at.strftime('%B %Y') if current_user.created_at else 'Unknown' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-item">
|
|
<span class="info-label">Authentication Method</span>
|
|
<span class="info-value">
|
|
{% if current_user.auth_provider == 'azure_ad' %}
|
|
<span class="badge bg-info"><i class="fab fa-microsoft me-1"></i>Microsoft SSO</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary"><i class="fas fa-key me-1"></i>Local (Email/Password)</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Change Section (only for local users) -->
|
|
{% if current_user.auth_provider != 'azure_ad' %}
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0"><i class="fas fa-key me-2"></i>Change Password</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="changePasswordForm">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="currentPassword" class="form-label">Current Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="currentPassword" required placeholder="Enter your current password">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="newPassword" class="form-label">New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="newPassword" required minlength="8" placeholder="Minimum 8 characters">
|
|
<div class="form-text">Password must be at least 8 characters.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="confirmNewPassword" class="form-label">Confirm New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="confirmNewPassword" required minlength="8" placeholder="Confirm your new password">
|
|
</div>
|
|
<div id="passwordChangeAlert" class="alert d-none mb-3" role="alert"></div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save me-2"></i>Update Password
|
|
</button>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="alert alert-info">
|
|
<h6><i class="fas fa-info-circle me-2"></i>Password Tips</h6>
|
|
<ul class="mb-0 small">
|
|
<li>Use at least 8 characters</li>
|
|
<li>Include a mix of letters, numbers, and symbols</li>
|
|
<li>Avoid using personal information</li>
|
|
<li>Don't reuse passwords from other sites</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Account Overview -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Account Overview</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3 text-center">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalAgents">-</div>
|
|
<div class="stat-label">Total Agents</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 text-center">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="activeAgents">-</div>
|
|
<div class="stat-label">Active Agents</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 text-center">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="developmentAgents">-</div>
|
|
<div class="stat-label">In Development</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 text-center">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="lastLoginDate">-</div>
|
|
<div class="stat-label">Last Activity</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>Recent Activity</h5>
|
|
<a href="{{ base_path }}/agent-management?view=my" class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-robot me-1"></i>View All My Agents
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="recentActivity">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.profile-avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 8px;
|
|
background: var(--brand-yellow);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--brand-dark);
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 2rem;
|
|
font-weight: 800;
|
|
margin: 0 auto 1rem;
|
|
}
|
|
|
|
.info-item {
|
|
padding: 0.75rem 0;
|
|
}
|
|
|
|
.info-label {
|
|
display: block;
|
|
font-weight: 500;
|
|
color: var(--text-light);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.info-value {
|
|
display: block;
|
|
color: var(--text-dark);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 1rem;
|
|
border-radius: 12px;
|
|
background: rgba(102, 126, 234, 0.05);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-light);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.recent-activity-item {
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.recent-activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.activity-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
color: white;
|
|
}
|
|
|
|
.activity-created { background-color: #28a745; }
|
|
.activity-updated { background-color: #ffc107; }
|
|
.activity-deleted { background-color: #dc3545; }
|
|
|
|
@media (max-width: 768px) {
|
|
.info-item {
|
|
padding: 0.5rem 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let userAgents = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadUserStats();
|
|
loadRecentActivity();
|
|
|
|
// Set up password change form handler if it exists
|
|
const changePasswordForm = document.getElementById('changePasswordForm');
|
|
if (changePasswordForm) {
|
|
changePasswordForm.addEventListener('submit', handlePasswordChange);
|
|
}
|
|
});
|
|
|
|
async function handlePasswordChange(e) {
|
|
e.preventDefault();
|
|
|
|
const currentPassword = document.getElementById('currentPassword').value;
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
|
|
const alertDiv = document.getElementById('passwordChangeAlert');
|
|
|
|
// Reset alert
|
|
alertDiv.classList.add('d-none');
|
|
alertDiv.classList.remove('alert-success', 'alert-danger');
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== confirmNewPassword) {
|
|
showPasswordAlert('New passwords do not match', 'danger');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (newPassword.length < 8) {
|
|
showPasswordAlert('New password must be at least 8 characters', 'danger');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/users/change-password', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
showPasswordAlert('Password changed successfully!', 'success');
|
|
// Clear the form
|
|
document.getElementById('changePasswordForm').reset();
|
|
} else {
|
|
const error = await response.json();
|
|
showPasswordAlert(error.detail || 'Failed to change password', 'danger');
|
|
}
|
|
} catch (error) {
|
|
showPasswordAlert('Failed to change password. Please try again.', 'danger');
|
|
}
|
|
}
|
|
|
|
function showPasswordAlert(message, type) {
|
|
const alertDiv = document.getElementById('passwordChangeAlert');
|
|
alertDiv.textContent = message;
|
|
alertDiv.classList.remove('d-none', 'alert-success', 'alert-danger');
|
|
alertDiv.classList.add(`alert-${type}`);
|
|
}
|
|
|
|
async function loadUserStats() {
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/agents', {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
userAgents = await response.json();
|
|
updateStats();
|
|
} else if (response.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
} else {
|
|
throw new Error('Failed to load agent statistics');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
document.getElementById('totalAgents').textContent = 'Error';
|
|
}
|
|
}
|
|
|
|
function updateStats() {
|
|
const totalAgents = userAgents.length;
|
|
const activeAgents = userAgents.filter(a => a.agent_status === 'Active').length;
|
|
const developmentAgents = userAgents.filter(a => a.agent_status === 'Development').length;
|
|
|
|
document.getElementById('totalAgents').textContent = totalAgents;
|
|
document.getElementById('activeAgents').textContent = activeAgents;
|
|
document.getElementById('developmentAgents').textContent = developmentAgents;
|
|
document.getElementById('lastLoginDate').textContent = 'Today';
|
|
}
|
|
|
|
function loadRecentActivity() {
|
|
if (userAgents.length === 0) {
|
|
setTimeout(loadRecentActivity, 500);
|
|
return;
|
|
}
|
|
|
|
// Sort agents by last updated date
|
|
const recentAgents = [...userAgents]
|
|
.sort((a, b) => new Date(b.agent_updated_at || b.agent_created_at) - new Date(a.agent_updated_at || a.agent_created_at))
|
|
.slice(0, 5);
|
|
|
|
if (recentAgents.length === 0) {
|
|
document.getElementById('recentActivity').innerHTML = `
|
|
<div class="text-center py-4">
|
|
<div class="mb-3">
|
|
<i class="fas fa-robot fa-2x text-muted"></i>
|
|
</div>
|
|
<h6>No Agent Activity Yet</h6>
|
|
<p class="text-muted mb-3">Create your first agent to see activity here</p>
|
|
<a href="{{ base_path }}/agent-register" class="btn btn-success">
|
|
<i class="fas fa-plus me-2"></i>Create Your First Agent
|
|
</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const activityHtml = recentAgents.map(agent => {
|
|
const isNew = !agent.agent_updated_at || agent.agent_updated_at === agent.agent_created_at;
|
|
const activityType = isNew ? 'created' : 'updated';
|
|
const activityDate = new Date(agent.agent_updated_at || agent.agent_created_at);
|
|
|
|
return `
|
|
<div class="recent-activity-item">
|
|
<div class="d-flex align-items-center">
|
|
<div class="activity-icon activity-${activityType} me-3">
|
|
<i class="fas ${isNew ? 'fa-plus' : 'fa-edit'}"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="mb-1">${agent.agent_name}</h6>
|
|
<small class="text-muted">Agent ${activityType} • ${formatRelativeDate(activityDate)}</small>
|
|
</div>
|
|
<span class="badge bg-${getStatusColor(agent.agent_status)}">${agent.agent_status || 'Development'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('recentActivity').innerHTML = activityHtml;
|
|
}
|
|
|
|
function formatRelativeDate(date) {
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins} minutes ago`;
|
|
if (diffHours < 24) return `${diffHours} hours ago`;
|
|
if (diffDays < 7) return `${diffDays} days ago`;
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
function getStatusColor(status) {
|
|
const colors = {
|
|
'Active': 'success',
|
|
'Development': 'warning',
|
|
'Inactive': 'secondary',
|
|
'Deprecated': 'danger'
|
|
};
|
|
return colors[status] || 'secondary';
|
|
}
|
|
</script>
|
|
{% endblock %} |