Docker compose healthchecks target /health on combined-api, dashboard-api and portal, but these endpoints did not exist (returned 404). This caused all dependent services (condition: service_healthy) to fail startup. - combined_api.py: GET /health + /turf/health with DB connectivity check - dashboard_api.py: GET /health + /turf/health with DB connectivity check - portal_server.py: GET /health (lightweight, no DB) QA Finding 1 from HRT-34 review of HRT-33 branch feature/devops-cicd. Co-Authored-By: Paperclip <noreply@paperclip.ing>
384 lines
20 KiB
HTML
384 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mon Compte — Turf IA</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
:root {
|
|
--green: #00c853; --green-d: #009624; --blue: #1e88e5;
|
|
--dark: #0d1117; --dark2: #161b22; --dark3: #21262d;
|
|
--text: #e6edf3; --muted: #8b949e; --border: #30363d;
|
|
--radius: 10px; --error: #f85149; --gold: #ffd600;
|
|
}
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--dark); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
|
|
a { color: inherit; text-decoration: none; }
|
|
nav { display: flex; align-items: center; justify-content: space-between; padding: 14px 5%; border-bottom: 1px solid var(--border); }
|
|
.nav-logo { font-weight: 700; font-size: 1.1rem; }
|
|
.nav-back { color: var(--muted); font-size: .9rem; }
|
|
.nav-back:hover { color: var(--text); }
|
|
|
|
main { flex: 1; padding: 40px 5%; max-width: 900px; margin: 0 auto; width: 100%; }
|
|
h1 { font-size: 1.6rem; font-weight: 800; margin-bottom: 28px; }
|
|
|
|
/* TABS */
|
|
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 28px; }
|
|
.tab-btn {
|
|
padding: 10px 18px; border: none; background: transparent; color: var(--muted);
|
|
font-size: .9rem; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent;
|
|
transition: all .2s; margin-bottom: -1px;
|
|
}
|
|
.tab-btn:hover { color: var(--text); }
|
|
.tab-btn.active { color: var(--green); border-bottom-color: var(--green); }
|
|
.tab-panel { display: none; }
|
|
.tab-panel.active { display: block; }
|
|
|
|
/* CARDS */
|
|
.card { background: var(--dark2); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-bottom: 20px; }
|
|
.card-title { font-size: 1rem; font-weight: 700; margin-bottom: 18px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
|
.form-group { margin-bottom: 16px; }
|
|
label { display: block; font-size: .83rem; font-weight: 600; color: var(--muted); margin-bottom: 6px; }
|
|
input {
|
|
width: 100%; padding: 10px 14px; background: var(--dark3);
|
|
border: 1px solid var(--border); border-radius: 8px;
|
|
color: var(--text); font-size: .9rem; outline: none; transition: border-color .2s;
|
|
}
|
|
input:focus { border-color: var(--green); }
|
|
input:disabled { opacity: .5; cursor: not-allowed; }
|
|
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 9px 20px; border-radius: 8px; font-size: .88rem; font-weight: 600; cursor: pointer; border: none; transition: all .2s; }
|
|
.btn-primary { background: var(--green); color: #000; }
|
|
.btn-primary:hover { background: var(--green-d); }
|
|
.btn-primary:disabled { opacity: .6; cursor: not-allowed; }
|
|
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
|
.btn-ghost:hover { border-color: var(--muted); }
|
|
.btn-danger { background: transparent; border: 1px solid var(--error); color: var(--error); }
|
|
.btn-danger:hover { background: rgba(248,81,73,.1); }
|
|
.btn-upgrade { background: linear-gradient(135deg, var(--gold), #ff6d00); color: #000; }
|
|
|
|
/* PLAN CARDS */
|
|
.plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 24px; }
|
|
.plan-card { background: var(--dark3); border: 2px solid var(--border); border-radius: var(--radius); padding: 20px; position: relative; transition: border-color .2s; }
|
|
.plan-card.current { border-color: var(--green); }
|
|
.plan-current-badge { position: absolute; top: -10px; left: 16px; background: var(--green); color: #000; padding: 2px 10px; border-radius: 10px; font-size: .7rem; font-weight: 700; }
|
|
.plan-name { font-weight: 800; font-size: 1rem; margin-bottom: 6px; }
|
|
.plan-price { font-size: 1.5rem; font-weight: 800; color: var(--green); }
|
|
.plan-price span { font-size: .85rem; font-weight: 400; color: var(--muted); }
|
|
.plan-features-mini { list-style: none; margin-top: 12px; }
|
|
.plan-features-mini li { font-size: .8rem; color: var(--muted); padding: 3px 0; }
|
|
.plan-features-mini li::before { content: "✓ "; color: var(--green); }
|
|
.plan-card .btn { width: 100%; justify-content: center; margin-top: 14px; font-size: .82rem; padding: 8px; }
|
|
|
|
/* ALERT */
|
|
.alert { padding: 12px 16px; border-radius: 8px; font-size: .88rem; margin-bottom: 14px; display: none; }
|
|
.alert.show { display: block; }
|
|
.alert-success { background: rgba(0,200,83,.1); border: 1px solid rgba(0,200,83,.3); color: var(--green); }
|
|
.alert-error { background: rgba(248,81,73,.1); border: 1px solid rgba(248,81,73,.3); color: var(--error); }
|
|
|
|
/* DANGER ZONE */
|
|
.danger-card { border-color: rgba(248,81,73,.3); }
|
|
|
|
/* Toast */
|
|
#toast { position: fixed; bottom: 24px; right: 24px; z-index: 999; padding: 12px 20px; border-radius: 10px; font-size: .88rem; font-weight: 600; transform: translateY(60px); opacity: 0; transition: all .3s; pointer-events: none; }
|
|
#toast.show { transform: translateY(0); opacity: 1; }
|
|
#toast.success { background: var(--green); color: #000; }
|
|
#toast.error { background: var(--error); color: #fff; }
|
|
|
|
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } main { padding: 20px 4%; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<a href="/" class="nav-logo">🏇 Turf IA</a>
|
|
<a href="/dashboard" class="nav-back">← Retour au dashboard</a>
|
|
</nav>
|
|
<main>
|
|
<h1>⚙️ Mon compte</h1>
|
|
|
|
<div class="tabs">
|
|
<button class="tab-btn active" data-tab="profile">Profil</button>
|
|
<button class="tab-btn" data-tab="security">Sécurité</button>
|
|
<button class="tab-btn" data-tab="upgrade">Mon plan</button>
|
|
<button class="tab-btn" data-tab="notifications">Notifications</button>
|
|
</div>
|
|
|
|
<!-- PROFIL -->
|
|
<div class="tab-panel active" id="tab-profile">
|
|
<div class="card">
|
|
<div class="card-title">Informations personnelles</div>
|
|
<div class="alert alert-success" id="profile-success">Profil mis à jour avec succès.</div>
|
|
<div class="alert alert-error" id="profile-error"></div>
|
|
<form id="profile-form">
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Prénom</label><input type="text" id="p-firstname" placeholder="Jean"></div>
|
|
<div class="form-group"><label>Nom</label><input type="text" id="p-lastname" placeholder="Dupont"></div>
|
|
</div>
|
|
<div class="form-group"><label>Adresse email</label><input type="email" id="p-email" placeholder="vous@exemple.fr"></div>
|
|
<button type="submit" class="btn btn-primary" id="profile-btn">Enregistrer les modifications</button>
|
|
</form>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Informations de compte</div>
|
|
<div class="form-group"><label>Plan actuel</label><input type="text" id="p-plan-display" disabled></div>
|
|
<div class="form-group"><label>Membre depuis</label><input type="text" id="p-created" disabled></div>
|
|
<div class="form-group"><label>Identifiant utilisateur</label><input type="text" id="p-id" disabled></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SECURITE -->
|
|
<div class="tab-panel" id="tab-security">
|
|
<div class="card">
|
|
<div class="card-title">Changer le mot de passe</div>
|
|
<div class="alert alert-success" id="pwd-success">Mot de passe mis à jour.</div>
|
|
<div class="alert alert-error" id="pwd-error"></div>
|
|
<form id="password-form">
|
|
<div class="form-group"><label>Mot de passe actuel</label><input type="password" id="s-current-pwd" placeholder="••••••••" autocomplete="current-password" required></div>
|
|
<div class="form-group"><label>Nouveau mot de passe</label><input type="password" id="s-new-pwd" placeholder="8 caractères minimum" autocomplete="new-password" required></div>
|
|
<div class="form-group"><label>Confirmer le nouveau mot de passe</label><input type="password" id="s-confirm-pwd" placeholder="••••••••" autocomplete="new-password" required></div>
|
|
<button type="submit" class="btn btn-primary" id="pwd-btn">Mettre à jour le mot de passe</button>
|
|
</form>
|
|
</div>
|
|
<div class="card danger-card">
|
|
<div class="card-title">Zone dangereuse</div>
|
|
<p style="font-size:.9rem;color:var(--muted);margin-bottom:16px">La suppression de votre compte est irréversible. Toutes vos données seront perdues.</p>
|
|
<button class="btn btn-danger" id="delete-account-btn">Supprimer mon compte</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- UPGRADE / PLAN -->
|
|
<div class="tab-panel" id="tab-upgrade">
|
|
<div class="card">
|
|
<div class="card-title">Votre plan actuel</div>
|
|
<div class="plan-grid">
|
|
<div class="plan-card" id="plan-card-free">
|
|
<div class="plan-name">Free</div>
|
|
<div class="plan-price">0€ <span>/mois</span></div>
|
|
<ul class="plan-features-mini">
|
|
<li>1 course complète/jour</li>
|
|
<li>Aperçu Top-3</li>
|
|
</ul>
|
|
<button class="btn btn-ghost" id="select-free-btn">Plan actuel</button>
|
|
</div>
|
|
<div class="plan-card" id="plan-card-premium">
|
|
<div class="plan-name">Premium ⭐</div>
|
|
<div class="plan-price">9,90€ <span>/mois</span></div>
|
|
<ul class="plan-features-mini">
|
|
<li>Toutes les courses</li>
|
|
<li>Alertes Telegram</li>
|
|
<li>Value bets</li>
|
|
<li>Historique 90j</li>
|
|
</ul>
|
|
<button class="btn btn-upgrade" id="select-premium-btn">Choisir Premium</button>
|
|
</div>
|
|
<div class="plan-card" id="plan-card-pro">
|
|
<div class="plan-name">Pro 🚀</div>
|
|
<div class="plan-price">24,90€ <span>/mois</span></div>
|
|
<ul class="plan-features-mini">
|
|
<li>Tout Premium</li>
|
|
<li>Export CSV</li>
|
|
<li>API REST</li>
|
|
<li>Support prioritaire</li>
|
|
</ul>
|
|
<button class="btn btn-ghost" id="select-pro-btn">Choisir Pro</button>
|
|
</div>
|
|
</div>
|
|
<p style="font-size:.82rem;color:var(--muted)">💳 Paiement sécurisé via Stripe (disponible dans le Sprint 5-6). Pour l'instant, contactez-nous pour activer un plan payant.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NOTIFICATIONS -->
|
|
<div class="tab-panel" id="tab-notifications">
|
|
<div class="card">
|
|
<div class="card-title">Alertes Telegram</div>
|
|
<div class="alert alert-success" id="notif-success">Préférences enregistrées.</div>
|
|
<form id="notif-form">
|
|
<div class="form-group">
|
|
<label>Chat ID Telegram</label>
|
|
<input type="text" id="n-telegram-id" placeholder="Votre Chat ID (ex: 123456789)">
|
|
<p style="font-size:.78rem;color:var(--muted);margin-top:5px">Envoyez /start à @TurfIABot pour obtenir votre Chat ID.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<p style="font-size:.88rem;font-weight:600;margin-bottom:10px;color:var(--muted)">Recevoir des alertes pour :</p>
|
|
<label style="display:flex;gap:10px;align-items:center;margin-bottom:10px;cursor:pointer;font-size:.9rem">
|
|
<input type="checkbox" id="n-alert-vb" style="width:auto;accent-color:var(--green)"> Value bets identifiés
|
|
</label>
|
|
<label style="display:flex;gap:10px;align-items:center;margin-bottom:10px;cursor:pointer;font-size:.9rem">
|
|
<input type="checkbox" id="n-alert-top1" style="width:auto;accent-color:var(--green)"> Favori IA Top-1
|
|
</label>
|
|
<label style="display:flex;gap:10px;align-items:center;cursor:pointer;font-size:.9rem">
|
|
<input type="checkbox" id="n-alert-quinte" style="width:auto;accent-color:var(--green)"> Quinté+ uniquement
|
|
</label>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Enregistrer les préférences</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<div id="toast"></div>
|
|
|
|
<script>
|
|
const API = '/api/v1';
|
|
function getToken() { return localStorage.getItem('turf_token'); }
|
|
|
|
async function fetchJson(url, opts = {}) {
|
|
const token = getToken();
|
|
const res = await fetch(url, {
|
|
...opts,
|
|
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(opts.headers || {}) }
|
|
});
|
|
if (res.status === 401) { location.href = '/login'; return null; }
|
|
return res.json().catch(() => null);
|
|
}
|
|
|
|
function showToast(msg, type = 'success') {
|
|
const t = document.getElementById('toast');
|
|
t.textContent = msg; t.className = `show ${type}`;
|
|
setTimeout(() => t.className = '', 3500);
|
|
}
|
|
|
|
function showAlert(id, show = true) {
|
|
document.getElementById(id)?.classList.toggle('show', show);
|
|
}
|
|
function setAlertMsg(id, msg, show = true) {
|
|
const el = document.getElementById(id);
|
|
if (el) { el.textContent = msg; el.classList.toggle('show', show); }
|
|
}
|
|
|
|
// TABS
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById(`tab-${btn.dataset.tab}`)?.classList.add('active');
|
|
});
|
|
});
|
|
|
|
// URL param: ?tab=upgrade
|
|
(function() {
|
|
const tab = new URLSearchParams(location.search).get('tab');
|
|
if (tab) {
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === `tab-${tab}`));
|
|
}
|
|
})();
|
|
|
|
// Load user
|
|
async function loadUser() {
|
|
if (!getToken()) { location.href = '/login'; return; }
|
|
const data = await fetchJson(`${API}/auth/me`);
|
|
if (!data) return;
|
|
const user = data.user || data;
|
|
document.getElementById('p-firstname').value = user.firstname || '';
|
|
document.getElementById('p-lastname').value = user.lastname || '';
|
|
document.getElementById('p-email').value = user.email || '';
|
|
document.getElementById('p-plan-display').value = { free: 'Free', premium: 'Premium', pro: 'Pro' }[user.plan] || 'Free';
|
|
document.getElementById('p-created').value = user.created_at ? new Date(user.created_at).toLocaleDateString('fr-FR') : '—';
|
|
document.getElementById('p-id').value = user.id || '—';
|
|
document.getElementById('n-telegram-id').value = user.telegram_chat_id || '';
|
|
document.getElementById('n-alert-vb').checked = user.alert_value_bets !== false;
|
|
document.getElementById('n-alert-top1').checked = user.alert_top1 !== false;
|
|
document.getElementById('n-alert-quinte').checked = !!user.alert_quinte_only;
|
|
setPlanCards(user.plan || 'free');
|
|
localStorage.setItem('turf_user', JSON.stringify(user));
|
|
}
|
|
|
|
function setPlanCards(plan) {
|
|
['free','premium','pro'].forEach(p => {
|
|
const card = document.getElementById(`plan-card-${p}`);
|
|
if (!card) return;
|
|
card.classList.toggle('current', p === plan);
|
|
const existing = card.querySelector('.plan-current-badge');
|
|
if (p === plan && !existing) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'plan-current-badge'; badge.textContent = '✓ Actuel';
|
|
card.prepend(badge);
|
|
} else if (p !== plan && existing) existing.remove();
|
|
});
|
|
}
|
|
|
|
// Profile form
|
|
document.getElementById('profile-form').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
showAlert('profile-success', false); showAlert('profile-error', false);
|
|
const btn = document.getElementById('profile-btn');
|
|
btn.disabled = true; btn.textContent = 'Enregistrement…';
|
|
const res = await fetchJson(`${API}/auth/update-profile`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
firstname: document.getElementById('p-firstname').value.trim(),
|
|
lastname: document.getElementById('p-lastname').value.trim(),
|
|
email: document.getElementById('p-email').value.trim()
|
|
})
|
|
});
|
|
btn.disabled = false; btn.textContent = 'Enregistrer les modifications';
|
|
if (res && res.ok !== false) { showAlert('profile-success'); showToast('Profil mis à jour.'); }
|
|
else setAlertMsg('profile-error', res?.error || 'Erreur lors de la mise à jour.');
|
|
});
|
|
|
|
// Password form
|
|
document.getElementById('password-form').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
showAlert('pwd-success', false); showAlert('pwd-error', false);
|
|
const np = document.getElementById('s-new-pwd').value;
|
|
const cp = document.getElementById('s-confirm-pwd').value;
|
|
if (np !== cp) { setAlertMsg('pwd-error', 'Les mots de passe ne correspondent pas.'); return; }
|
|
if (np.length < 8) { setAlertMsg('pwd-error', 'Minimum 8 caractères.'); return; }
|
|
const btn = document.getElementById('pwd-btn');
|
|
btn.disabled = true; btn.textContent = 'Mise à jour…';
|
|
const res = await fetchJson(`${API}/auth/change-password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ current_password: document.getElementById('s-current-pwd').value, new_password: np })
|
|
});
|
|
btn.disabled = false; btn.textContent = 'Mettre à jour le mot de passe';
|
|
if (res && res.ok !== false) { showAlert('pwd-success'); showToast('Mot de passe mis à jour.'); document.getElementById('password-form').reset(); }
|
|
else setAlertMsg('pwd-error', res?.error || 'Mot de passe actuel incorrect.');
|
|
});
|
|
|
|
// Notifications form
|
|
document.getElementById('notif-form').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
showAlert('notif-success', false);
|
|
const res = await fetchJson(`${API}/auth/update-preferences`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
telegram_chat_id: document.getElementById('n-telegram-id').value.trim(),
|
|
alert_value_bets: document.getElementById('n-alert-vb').checked,
|
|
alert_top1: document.getElementById('n-alert-top1').checked,
|
|
alert_quinte_only: document.getElementById('n-alert-quinte').checked
|
|
})
|
|
});
|
|
if (res && res.ok !== false) { showAlert('notif-success'); showToast('Préférences enregistrées.'); }
|
|
else showToast('Erreur lors de la sauvegarde.', 'error');
|
|
});
|
|
|
|
// Plan selection (placeholder until Stripe Sprint 5-6)
|
|
['free','premium','pro'].forEach(p => {
|
|
document.getElementById(`select-${p}-btn`)?.addEventListener('click', () => {
|
|
if (p === 'free') {
|
|
showToast('Vous êtes déjà sur le plan Free.', 'info');
|
|
} else {
|
|
showToast('Paiement Stripe disponible dans le Sprint 5-6. Contactez-nous pour activer ce plan.', 'info');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Delete account
|
|
document.getElementById('delete-account-btn').addEventListener('click', () => {
|
|
if (confirm('⚠️ Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.')) {
|
|
if (confirm('Confirmez la suppression définitive de votre compte Turf IA.')) {
|
|
fetchJson(`${API}/auth/delete-account`, { method: 'DELETE' }).then(() => {
|
|
localStorage.clear();
|
|
location.href = '/';
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
loadUser();
|
|
</script>
|
|
</body>
|
|
</html>
|