|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Générateur de Comptes GabaoHub</title> |
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> |
|
<style> |
|
body { |
|
padding-top: 2rem; |
|
background-color: #f8f9fa; |
|
} |
|
.card { |
|
margin-bottom: 20px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
border-radius: 10px; |
|
overflow: hidden; |
|
} |
|
.card-header { |
|
background-color: #4c5daf; |
|
color: white; |
|
padding: 1rem; |
|
} |
|
.success { |
|
background-color: #d4edda; |
|
} |
|
.failed { |
|
background-color: #f8d7da; |
|
} |
|
.pending { |
|
background-color: #fff3cd; |
|
} |
|
.account-details { |
|
font-family: monospace; |
|
font-size: 0.9rem; |
|
} |
|
.progress { |
|
height: 25px; |
|
border-radius: 15px; |
|
} |
|
#status-badge { |
|
font-size: 1rem; |
|
} |
|
.settings-panel { |
|
background-color: #f1f3f9; |
|
border-radius: 8px; |
|
padding: 15px; |
|
margin-bottom: 20px; |
|
} |
|
.copy-btn { |
|
cursor: pointer; |
|
} |
|
.export-options { |
|
margin-bottom: 15px; |
|
} |
|
.stats-card { |
|
background-color: #e9ecef; |
|
border-radius: 8px; |
|
padding: 15px; |
|
margin-bottom: 15px; |
|
transition: all 0.3s ease; |
|
} |
|
.stats-card:hover { |
|
transform: translateY(-3px); |
|
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1); |
|
} |
|
.stats-value { |
|
font-size: 1.5rem; |
|
font-weight: bold; |
|
} |
|
.table-responsive { |
|
border-radius: 8px; |
|
overflow: hidden; |
|
} |
|
|
|
@keyframes highlight { |
|
0% { background-color: #fff9c4; } |
|
100% { background-color: transparent; } |
|
} |
|
.highlight-new { |
|
animation: highlight 2s ease-in-out; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="row justify-content-center"> |
|
<div class="col-md-10"> |
|
<div class="card"> |
|
<div class="card-header d-flex justify-content-between align-items-center"> |
|
<h2><i class="bi bi-person-plus-fill"></i> Générateur de Comptes GabaoHub</h2> |
|
<span id="status-badge" class="badge {% if creation_in_progress %}bg-warning{% else %}bg-success{% endif %}"> |
|
{% if creation_in_progress %} |
|
<i class="bi bi-hourglass-split"></i> En cours |
|
{% else %} |
|
<i class="bi bi-check-circle"></i> Prêt |
|
{% endif %} |
|
</span> |
|
</div> |
|
<div class="card-body"> |
|
<div class="settings-panel mb-4"> |
|
<h5><i class="bi bi-gear-fill"></i> Paramètres de création</h5> |
|
<form action="/start" method="post" class="mb-3" id="creation-form"> |
|
<div class="row g-3"> |
|
<div class="col-md-6"> |
|
<label for="num_accounts" class="form-label">Nombre de comptes</label> |
|
<div class="input-group"> |
|
<span class="input-group-text"><i class="bi bi-people-fill"></i></span> |
|
<input type="number" class="form-control" id="num_accounts" name="num_accounts" value="100" min="1" max="100000" {% if creation_in_progress %}disabled{% endif %}> |
|
</div> |
|
</div> |
|
<div class="col-md-6"> |
|
<label for="concurrency" class="form-label">Concurrence</label> |
|
<div class="input-group"> |
|
<span class="input-group-text"><i class="bi bi-speedometer"></i></span> |
|
<input type="number" class="form-control" id="concurrency" name="concurrency" value="5" min="1" max="10" {% if creation_in_progress %}disabled{% endif %}> |
|
<button type="submit" class="btn btn-primary" {% if creation_in_progress %}disabled{% endif %}> |
|
<i class="bi bi-play-fill"></i> Démarrer |
|
</button> |
|
</div> |
|
<small class="text-muted">Nombre de tâches simultanées (1-10)</small> |
|
</div> |
|
</div> |
|
</form> |
|
</div> |
|
|
|
{% if creation_in_progress or progress > 0 %} |
|
<div class="mb-4"> |
|
<h5><i class="bi bi-graph-up"></i> Progression</h5> |
|
<div class="progress mb-2"> |
|
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-primary" |
|
role="progressbar" style="width: {{ (progress / total * 100) if total > 0 else 0 }}%;" |
|
aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="{{ total }}"> |
|
{{ progress }}/{{ total }} |
|
</div> |
|
</div> |
|
<p id="progress-text" class="text-muted"> |
|
<i class="bi bi-info-circle"></i> {{ progress }} compte(s) sur {{ total }} créé(s) |
|
{% if creation_in_progress %} |
|
<span class="spinner-border spinner-border-sm text-primary ms-2" role="status"></span> |
|
{% endif %} |
|
</p> |
|
</div> |
|
|
|
<div class="row mb-4"> |
|
<div class="col-md-4"> |
|
<div class="stats-card text-center"> |
|
<i class="bi bi-check-circle-fill text-success mb-2" style="font-size: 1.5rem;"></i> |
|
<h6>Réussis</h6> |
|
<div id="success-count" class="stats-value text-success">{{ accounts|selectattr('status', 'equalto', 'success')|list|length }}</div> |
|
</div> |
|
</div> |
|
<div class="col-md-4"> |
|
<div class="stats-card text-center"> |
|
<i class="bi bi-x-circle-fill text-danger mb-2" style="font-size: 1.5rem;"></i> |
|
<h6>Échoués</h6> |
|
<div id="failed-count" class="stats-value text-danger">{{ accounts|selectattr('status', 'equalto', 'failed')|list|length }}</div> |
|
</div> |
|
</div> |
|
<div class="col-md-4"> |
|
<div class="stats-card text-center"> |
|
<i class="bi bi-hourglass-split text-warning mb-2" style="font-size: 1.5rem;"></i> |
|
<h6>En attente</h6> |
|
<div id="pending-count" class="stats-value text-warning">{{ accounts|selectattr('status', 'equalto', 'pending')|list|length }}</div> |
|
</div> |
|
</div> |
|
</div> |
|
{% endif %} |
|
|
|
{% if accounts and accounts|length > 0 %} |
|
<div> |
|
<div class="d-flex justify-content-between align-items-center mb-3"> |
|
<h5><i class="bi bi-person-lines-fill"></i> Comptes créés</h5> |
|
<div class="d-flex"> |
|
<div class="export-options me-2"> |
|
<div class="dropdown"> |
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="exportDropdown" data-bs-toggle="dropdown" aria-expanded="false"> |
|
<i class="bi bi-download"></i> Exporter |
|
</button> |
|
<ul class="dropdown-menu" aria-labelledby="exportDropdown"> |
|
<li><a class="dropdown-item" href="#" id="export-all"><i class="bi bi-file-earmark-text"></i> Tous les comptes</a></li> |
|
<li><a class="dropdown-item" href="#" id="export-success"><i class="bi bi-file-earmark-check"></i> Comptes réussis</a></li> |
|
<li><hr class="dropdown-divider"></li> |
|
<li><a class="dropdown-item" href="#" id="copy-table"><i class="bi bi-clipboard"></i> Copier le tableau</a></li> |
|
</ul> |
|
</div> |
|
</div> |
|
<form action="/reset" method="post" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer toutes les données?');"> |
|
<button type="submit" class="btn btn-danger" {% if creation_in_progress %}disabled{% endif %}> |
|
<i class="bi bi-trash"></i> Réinitialiser |
|
</button> |
|
</form> |
|
</div> |
|
</div> |
|
|
|
<div class="table-responsive"> |
|
<table class="table table-striped table-hover" id="accounts-table"> |
|
<thead class="table-dark"> |
|
<tr> |
|
<th>#</th> |
|
<th>Nom d'utilisateur</th> |
|
<th>Mot de passe</th> |
|
<th>Province</th> |
|
<th>Ville</th> |
|
<th>Statut</th> |
|
<th>Horodatage</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="accounts-table-body"> |
|
{% for account in accounts %} |
|
<tr class="{{ account.status }}"> |
|
<td>{{ loop.index }}</td> |
|
<td>{{ account.username }}</td> |
|
<td> |
|
<span class="password-text">{{ account.password }}</span> |
|
<i class="bi bi-clipboard copy-btn ms-2" data-clipboard-text="{{ account.password }}" title="Copier le mot de passe"></i> |
|
</td> |
|
<td>{{ account.province }}</td> |
|
<td>{{ account.ville }}</td> |
|
<td> |
|
<span class="badge {% if account.status == 'success' %}bg-success{% elif account.status == 'failed' %}bg-danger{% else %}bg-warning{% endif %}"> |
|
{% if account.status == 'success' %} |
|
<i class="bi bi-check-circle-fill"></i> |
|
{% elif account.status == 'failed' %} |
|
<i class="bi bi-x-circle-fill"></i> |
|
{% else %} |
|
<i class="bi bi-hourglass-split"></i> |
|
{% endif %} |
|
{{ account.status }} |
|
</span> |
|
</td> |
|
<td>{{ account.timestamp if account.timestamp else 'N/A' }}</td> |
|
<td> |
|
<button class="btn btn-sm btn-outline-primary copy-account" data-username="{{ account.username }}" data-password="{{ account.password }}"> |
|
<i class="bi bi-clipboard-check"></i> |
|
</button> |
|
</td> |
|
</tr> |
|
{% endfor %} |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
{% else %} |
|
<div class="alert alert-info"> |
|
<i class="bi bi-info-circle-fill"></i> Aucun compte n'a encore été créé. Cliquez sur "Démarrer" pour commencer. |
|
</div> |
|
{% endif %} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11"> |
|
<div id="copyToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> |
|
<div class="toast-header bg-success text-white"> |
|
<i class="bi bi-check-circle-fill me-2"></i> |
|
<strong class="me-auto">Notification</strong> |
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button> |
|
</div> |
|
<div class="toast-body" id="toast-message"> |
|
Copié avec succès! |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> |
|
<script> |
|
|
|
function showToast(message, type = 'success') { |
|
const toast = $('#copyToast'); |
|
const toastHeader = toast.find('.toast-header'); |
|
|
|
|
|
if (type === 'success') { |
|
toastHeader.removeClass('bg-danger').addClass('bg-success'); |
|
toastHeader.find('i').removeClass('bi-x-circle-fill').addClass('bi-check-circle-fill'); |
|
} else { |
|
toastHeader.removeClass('bg-success').addClass('bg-danger'); |
|
toastHeader.find('i').removeClass('bi-check-circle-fill').addClass('bi-x-circle-fill'); |
|
} |
|
|
|
|
|
$('#toast-message').text(message); |
|
|
|
|
|
const bsToast = new bootstrap.Toast(toast); |
|
bsToast.show(); |
|
} |
|
|
|
|
|
function copyToClipboard(text) { |
|
navigator.clipboard.writeText(text).then(function() { |
|
showToast('Copié avec succès!'); |
|
}, function() { |
|
showToast('Échec de la copie', 'error'); |
|
}); |
|
} |
|
|
|
|
|
function exportToCSV(accounts, onlySuccess = false) { |
|
if (!accounts || accounts.length === 0) return; |
|
|
|
|
|
const dataToExport = onlySuccess ? accounts.filter(a => a.status === 'success') : accounts; |
|
|
|
|
|
let csv = 'Username,Password,Province,Ville,Status,Timestamp\n'; |
|
|
|
|
|
dataToExport.forEach(account => { |
|
const username = account.username || ''; |
|
const password = account.password || ''; |
|
const province = account.province || ''; |
|
const ville = account.ville || ''; |
|
const status = account.status || ''; |
|
const timestamp = account.timestamp || ''; |
|
|
|
csv += `"${username}","${password}","${province}","${ville}","${status}","${timestamp}"\n`; |
|
}); |
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); |
|
const url = URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.setAttribute('href', url); |
|
link.setAttribute('download', onlySuccess ? 'gabao_hub_success_accounts.csv' : 'gabao_hub_all_accounts.csv'); |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
} |
|
|
|
|
|
function updateProgress() { |
|
if ({{ creation_in_progress|tojson }}) { |
|
$.ajax({ |
|
url: '/progress', |
|
type: 'GET', |
|
dataType: 'json', |
|
success: function(data) { |
|
|
|
if (data.creation_in_progress) { |
|
$('#status-badge').html('<i class="bi bi-hourglass-split"></i> En cours').removeClass('bg-success').addClass('bg-warning'); |
|
$('#creation-form button').prop('disabled', true); |
|
$('#num_accounts').prop('disabled', true); |
|
$('#concurrency').prop('disabled', true); |
|
$('form[action="/reset"] button').prop('disabled', true); |
|
} else { |
|
$('#status-badge').html('<i class="bi bi-check-circle"></i> Prêt').removeClass('bg-warning').addClass('bg-success'); |
|
$('#creation-form button').prop('disabled', false); |
|
$('#num_accounts').prop('disabled', false); |
|
$('#concurrency').prop('disabled', false); |
|
$('form[action="/reset"] button').prop('disabled', false); |
|
} |
|
|
|
|
|
let percentage = data.total > 0 ? (data.progress / data.total * 100) : 0; |
|
$('#progress-bar').css('width', percentage + '%').attr('aria-valuenow', data.progress); |
|
$('#progress-bar').text(data.progress + '/' + data.total); |
|
|
|
let progressText = data.progress + ' compte(s) sur ' + data.total + ' créé(s)'; |
|
if (data.creation_in_progress) { |
|
progressText += ' <span class="spinner-border spinner-border-sm text-primary ms-2" role="status"></span>'; |
|
} |
|
$('#progress-text').html('<i class="bi bi-info-circle"></i> ' + progressText); |
|
|
|
|
|
updateStatCounters(data.accounts); |
|
|
|
|
|
updateAccountsTable(data.accounts); |
|
|
|
|
|
if (data.creation_in_progress) { |
|
setTimeout(updateProgress, 2000); |
|
} else { |
|
|
|
location.reload(); |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
function updateStatCounters(accounts) { |
|
if (!accounts || accounts.length === 0) return; |
|
|
|
const successCount = accounts.filter(a => a.status === 'success').length; |
|
const failedCount = accounts.filter(a => a.status === 'failed').length; |
|
const pendingCount = accounts.filter(a => a.status === 'pending').length; |
|
|
|
$('#success-count').text(successCount); |
|
$('#failed-count').text(failedCount); |
|
$('#pending-count').text(pendingCount); |
|
} |
|
|
|
|
|
function updateAccountsTable(accounts) { |
|
if (!accounts || accounts.length === 0) return; |
|
|
|
let tableBody = $('#accounts-table-body'); |
|
let existingRows = tableBody.find('tr').length; |
|
|
|
|
|
accounts.forEach(function(account, index) { |
|
let statusClass = ''; |
|
let statusIcon = ''; |
|
let statusBadge = ''; |
|
|
|
if (account.status === 'success') { |
|
statusClass = 'success'; |
|
statusIcon = '<i class="bi bi-check-circle-fill"></i>'; |
|
statusBadge = '<span class="badge bg-success">' + statusIcon + ' success</span>'; |
|
} else if (account.status === 'failed') { |
|
statusClass = 'failed'; |
|
statusIcon = '<i class="bi bi-x-circle-fill"></i>'; |
|
statusBadge = '<span class="badge bg-danger">' + statusIcon + ' failed</span>'; |
|
} else { |
|
statusClass = 'pending'; |
|
statusIcon = '<i class="bi bi-hourglass-split"></i>'; |
|
statusBadge = '<span class="badge bg-warning">' + statusIcon + ' pending</span>'; |
|
} |
|
|
|
const timestamp = account.timestamp ? account.timestamp : 'N/A'; |
|
const username = account.username || ''; |
|
const password = account.password || ''; |
|
const province = account.province || ''; |
|
const ville = account.ville || ''; |
|
|
|
|
|
let existingRow = tableBody.find('tr').eq(index); |
|
|
|
if (existingRow.length) { |
|
|
|
existingRow.attr('class', statusClass); |
|
existingRow.find('td').eq(1).text(username); |
|
|
|
let passwordCell = existingRow.find('td').eq(2); |
|
passwordCell.html('<span class="password-text">' + password + '</span><i class="bi bi-clipboard copy-btn ms-2" data-clipboard-text="' + password + '" title="Copier le mot de passe"></i>'); |
|
|
|
existingRow.find('td').eq(3).text(province); |
|
existingRow.find('td').eq(4).text(ville); |
|
existingRow.find('td').eq(5).html(statusBadge); |
|
existingRow.find('td').eq(6).text(timestamp); |
|
|
|
|
|
let actionsCell = existingRow.find('td').eq(7); |
|
actionsCell.html('<button class="btn btn-sm btn-outline-primary copy-account" data-username="' + username + '" data-password="' + password + '"><i class="bi bi-clipboard-check"></i></button>'); |
|
|
|
|
|
if (existingRow.data('status') === 'pending' && account.status !== 'pending') { |
|
existingRow.addClass('highlight-new'); |
|
setTimeout(function() { |
|
existingRow.removeClass('highlight-new'); |
|
}, 2000); |
|
} |
|
|
|
|
|
existingRow.data('status', account.status); |
|
|
|
} else { |
|
|
|
let newRow = $(` |
|
<tr class="${statusClass}" data-status="${account.status}"> |
|
<td>${index + 1}</td> |
|
<td>${username}</td> |
|
<td> |
|
<span class="password-text">${password}</span> |
|
<i class="bi bi-clipboard copy-btn ms-2" data-clipboard-text="${password}" title="Copier le mot de passe"></i> |
|
</td> |
|
<td>${province}</td> |
|
<td>${ville}</td> |
|
<td>${statusBadge}</td> |
|
<td>${timestamp}</td> |
|
<td> |
|
<button class="btn btn-sm btn-outline-primary copy-account" data-username="${username}" data-password="${password}"> |
|
<i class="bi bi-clipboard-check"></i> |
|
</button> |
|
</td> |
|
</tr> |
|
`); |
|
|
|
tableBody.append(newRow); |
|
newRow.addClass('highlight-new'); |
|
setTimeout(function() { |
|
newRow.removeClass('highlight-new'); |
|
}, 2000); |
|
} |
|
}); |
|
} |
|
|
|
|
|
$(document).ready(function() { |
|
|
|
|
|
|
|
$(document).on('click', '.copy-btn', function() { |
|
const text = $(this).data('clipboard-text'); |
|
copyToClipboard(text); |
|
}); |
|
|
|
|
|
$(document).on('click', '.copy-account', function() { |
|
const username = $(this).data('username'); |
|
const password = $(this).data('password'); |
|
const accountInfo = `Nom d'utilisateur: ${username}\nMot de passe: ${password}`; |
|
copyToClipboard(accountInfo); |
|
}); |
|
|
|
|
|
$('#export-all').click(function(e) { |
|
e.preventDefault(); |
|
$.ajax({ |
|
url: '/progress', |
|
type: 'GET', |
|
dataType: 'json', |
|
success: function(data) { |
|
exportToCSV(data.accounts, false); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
$('#export-success').click(function(e) { |
|
e.preventDefault(); |
|
$.ajax({ |
|
url: '/progress', |
|
type: 'GET', |
|
dataType: 'json', |
|
success: function(data) { |
|
exportToCSV(data.accounts, true); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
$('#copy-table').click(function(e) { |
|
e.preventDefault(); |
|
|
|
|
|
let tableText = "Index\tNom d'utilisateur\tMot de passe\tProvince\tVille\tStatut\tHorodatage\n"; |
|
|
|
$('#accounts-table tbody tr').each(function() { |
|
const row = $(this); |
|
const index = row.find('td').eq(0).text(); |
|
const username = row.find('td').eq(1).text(); |
|
const password = row.find('.password-text').text(); |
|
const province = row.find('td').eq(3).text(); |
|
const ville = row.find('td').eq(4).text(); |
|
const status = row.hasClass('success') ? 'success' : (row.hasClass('failed') ? 'failed' : 'pending'); |
|
const timestamp = row.find('td').eq(6).text(); |
|
|
|
tableText += `${index}\t${username}\t${password}\t${province}\t${ville}\t${status}\t${timestamp}\n`; |
|
}); |
|
|
|
copyToClipboard(tableText); |
|
}); |
|
|
|
|
|
if ({{ creation_in_progress|tojson }}) { |
|
setTimeout(updateProgress, 1000); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |