agent_tracker/templates/profile.html
nickviljoen 08038b066f Rebrand UI to OLIVER template: Montserrat, yellow accents, sticky nav, tab fixes
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>
2026-05-17 10:12:18 +02:00

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