
Thanks to those who have leased from me on Hive Engine. I'll be adding more in a few days, and as you can see on the screen, by tweaking a few settings here and there in my automated curation, I'm starting to get good results.
The bot only votes for the best posts it identifies in its settings.
Minimum 300 words
Positive reputation
Not blacklisted
All topics
Conducts voting sessions at random times during the day, and only when voting is possible. Scans every 30 minutes during these sessions.
Vote between 150 and 200 posts per day at 5%.
The curation APRs are starting to work well. The bot isn't perfect yet; I'm still in research and development.

The rebots that are supposed to regulate the GLYPH token are also starting to run well; I'll have to work on improving all of this further, but it's managing to buy back almost as many tokens as the number distributed by the curation rebot, which sends 0.01 to each person it votes for (within 24 hours).

And for those interested, we're posting the complete code for the reboot, if anyone is better than me at rewriting it properly.
<?php
// ----------- CONFIGURATION -----------
define('VOTER_ACCOUNT', 'we-are-ai');
define('MIN_VP_THRESHOLD', 78);
define('MIN_WORD_COUNT', 300);
define('POSTS_PER_PAGE', 20);
define('DEBUG_MODE', true);
// ----------- AUTO-VOTE CONFIGURATION -----------
define('AUTO_VOTE_ENABLED', true); // ⚠️ Mettre false pour désactiver
define('AUTO_VOTE_DELAY', 5000); // Délai entre chaque vote (ms) 5 secondes
define('AUTO_REFRESH_AFTER_VOTE', true); // Refresh après avoir tout voté
// ========== 🎯 MODIFIE ICI TES SEUILS DE VOTE ==========
// Format: nombre_de_mots => poids_de_vote (en centièmes, 700 = 7%)
$VOTE_RULES = [
300 => 500 // 5% pour 350-499 mots
];
// ========================================================
// ----------- FONCTION DE CALCUL DE POIDS -----------
function get_vote_weight($word_count) {
global $VOTE_RULES;
foreach ($VOTE_RULES as $min_words => $weight) {
if ($word_count >= $min_words) return $weight;
}
return 0;
}
// ----------- COMPTEUR DE MOTS -----------
function count_words($text) {
$text = preg_replace('/^#+\s*.+/m', '', $text);
$text = preg_replace('/\[[^\]]*\]\([^)]*\)/', '', $text);
$text = preg_replace('/!\[[^\]]*\]\([^)]*\)/', '', $text);
$text = preg_replace('/`{1,3}[^`]+`{1,3}/', '', $text);
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
$text = preg_replace('/[^\p{L}\p{N}\s\']/u', ' ', $text);
$text = preg_replace('/\s+/', ' ', trim($text));
$words = explode(' ', $text);
return count($words);
}
// ----------- RÉCUPÉRATION DES POSTS -----------
function get_hive_posts() {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "bridge.get_ranked_posts",
"params" => [
"sort" => "created",
"tag" => "",
"observer" => VOTER_ACCOUNT,
"limit" => POSTS_PER_PAGE
],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$res = curl_exec($ch);
curl_close($ch);
$json = json_decode($res, true);
return $json['result'] ?? [];
}
// ----------- RÉCUPÉRATION COMPTE -----------
function get_hive_account($account) {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "condenser_api.get_accounts",
"params" => [[$account]],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$res = curl_exec($ch);
curl_close($ch);
$json = json_decode($res, true);
return $json['result'][0] ?? null;
}
// ----------- RÉCUPÉRATION DU VP -----------
function get_current_vp($account) {
$url = "https://api.hive.blog";
$payload = json_encode([
"jsonrpc" => "2.0",
"method" => "condenser_api.get_accounts",
"params" => [[$account]],
"id" => 1
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return isset($response['result'][0]['voting_power'])
? round($response['result'][0]['voting_power'] / 100, 1)
: 0;
}
// ----------- LOGIQUE PRINCIPALE -----------
$account = get_hive_account(VOTER_ACCOUNT);
$current_vp = get_current_vp(VOTER_ACCOUNT);
$posts = get_hive_posts();
$eligible_posts = [];
$debug_logs = [];
foreach ($posts as $p) {
$words = count_words($p['body']);
$vote_weight = get_vote_weight($words);
$user_voted = false;
$reason = "";
foreach ($p['active_votes'] ?? [] as $vote) {
if ($vote['voter'] === VOTER_ACCOUNT) {
$user_voted = true;
$reason = "Already voted";
break;
}
}
if ($words < MIN_WORD_COUNT) {
$reason = "Too short ($words words)";
} elseif ($user_voted) {
$reason = "Already voted";
} else {
$reason = "✅ Eligible";
}
$debug_logs[] = [
'author' => $p['author'],
'permlink' => $p['permlink'],
'words' => $words,
'reason' => $reason
];
if ($vote_weight > 0 && !$user_voted && $current_vp >= MIN_VP_THRESHOLD) {
$p['is_eligible'] = true;
$eligible_posts[] = $p;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Hive Auto Words</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600&family=Rajdhani:wght@500;600&display=swap" rel="stylesheet">
<style>
:root {
--neon-blue: #00d4ff;
--neon-purple: #b200ff;
--neon-green: #00ff88;
--neon-red: #ff2e63;
--neon-yellow: #ffc700;
--bg-dark: #0a0a1a;
--bg-card: #121230;
--text-primary: #e0e0ff;
--text-secondary: #a0a0c0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Rajdhani', sans-serif;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 100%);
color: var(--text-primary);
padding: 40px 20px;
min-height: 100vh;
}
h1 {
font-family: 'Orbitron', sans-serif;
text-align: center;
font-size: 2.5rem;
margin-bottom: 40px;
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.card {
background: rgba(18, 18, 48, 0.9);
border: 1px solid rgba(178, 0, 255, 0.2);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.card h2 {
font-family: 'Orbitron', sans-serif;
color: var(--neon-purple);
margin-bottom: 20px;
font-size: 1.5rem;
}
.card p {
font-size: 1.1rem;
margin-bottom: 12px;
color: var(--text-secondary);
}
.card strong {
color: var(--text-primary);
}
.vp-container {
background: rgba(10, 10, 30, 0.5);
padding: 15px;
border-radius: 10px;
margin: 15px 0;
}
.vp-bar {
width: 100%;
height: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.vp-fill {
height: 100%;
width: <?php echo $current_vp; ?>%;
background: linear-gradient(90deg, var(--neon-green), var(--neon-yellow));
transition: width 0.5s ease;
}
table {
width: 100%;
border-collapse: collapse;
background: rgba(18, 18, 48, 0.6);
border-radius: 10px;
overflow: hidden;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid rgba(178, 0, 255, 0.1);
}
th {
background: rgba(178, 0, 255, 0.1);
color: var(--neon-purple);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
tr:hover {
background: rgba(0, 212, 255, 0.05);
}
button {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple));
color: white;
box-shadow: 0 4px 6px rgba(0, 212, 255, 0.2);
position: relative;
overflow: hidden;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 212, 255, 0.3);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #3a3a5a;
color: #666;
cursor: not-allowed;
box-shadow: none;
}
button.voted {
background: linear-gradient(90deg, var(--neon-green), var(--neon-yellow));
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(0, 255, 136, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 255, 136, 0); }
}
.debug-table {
margin-top: 40px;
border-top: 1px solid rgba(178, 0, 255, 0.2);
padding-top: 30px;
}
.debug-table h2 {
color: var(--neon-purple);
text-shadow: 0 0 8px rgba(178, 0, 255, 0.3);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card, table, h1, h2 {
animation: fadeIn 0.5s ease-out forwards;
}
.history-item {
background: rgba(10, 10, 30, 0.7);
border: 1px solid rgba(178, 0, 255, 0.3);
border-radius: 10px;
padding: 15px;
display: grid;
grid-template-columns: 80px 1fr auto;
gap: 15px;
align-items: center;
transition: all 0.3s ease;
}
.history-item:hover {
background: rgba(0, 212, 255, 0.1);
border-color: var(--neon-blue);
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.2);
}
.history-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid var(--neon-purple);
object-fit: cover;
}
.history-content {
display: flex;
flex-direction: column;
gap: 5px;
}
.history-author {
font-weight: bold;
color: var(--neon-blue);
text-decoration: none;
font-size: 1.1rem;
}
.history-author:hover {
color: var(--neon-purple);
}
.history-post-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
}
.history-post-link:hover {
color: var(--neon-green);
}
.history-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.history-weight {
background: linear-gradient(90deg, var(--neon-purple), var(--neon-blue));
padding: 6px 12px;
border-radius: 6px;
font-weight: bold;
font-size: 0.95rem;
}
.history-time {
color: var(--text-secondary);
font-size: 0.85rem;
}
.status-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.status-success {
background: rgba(0, 255, 136, 0.2);
color: var(--neon-green);
border: 1px solid rgba(0, 255, 136, 0.3);
}
.status-failed {
background: rgba(255, 46, 99, 0.2);
color: var(--neon-red);
border: 1px solid rgba(255, 46, 99, 0.3);
}
.status-loading {
background: rgba(0, 212, 255, 0.2);
color: var(--neon-blue);
border: 1px solid rgba(0, 212, 255, 0.3);
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
border-color: rgba(0, 212, 255, 0.4);
}
50% {
box-shadow: 0 6px 20px rgba(0, 212, 255, 0.6);
border-color: rgba(0, 212, 255, 0.8);
}
}
@media (max-width: 768px) {
body { padding: 15px; }
.card { padding: 15px; }
th, td { padding: 8px 10px; }
h1 { font-size: 1.8rem; }
}
</style>
</head>
<body>
<h1>🤖 HIVE AUTO WORDS <span style="font-size: 1.2rem; font-weight: normal; color: var(--text-secondary);">Edition</span></h1>
<!-- ========== PATCH 2: Account Switcher ========== -->
<div class="card" style="position: sticky; top: 20px; z-index: 1000; margin-bottom: 30px;">
<h2 style="margin-bottom: 15px;">🔐 Voting Account</h2>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<input
type="text"
id="account-input"
placeholder="Enter Hive username"
style="flex: 1; padding: 10px; background: rgba(18, 18, 48, 0.8); border: 1px solid rgba(178, 0, 255, 0.3); border-radius: 8px; color: var(--text-primary); font-size: 1rem;"
/>
<button
id="connect-btn"
onclick="connectAccount()"
style="padding: 10px 20px; white-space: nowrap;">
🔗 Connect & Validate
</button>
</div>
<div id="account-status" style="display: none; padding: 10px; border-radius: 6px; margin-top: 10px; font-size: 0.9rem;"></div>
</div>
<div class="card">
<h2>📊 Current Status</h2>
<p><strong>Account:</strong> <span style="color: var(--neon-purple);" id="current-account-display">@<?php echo VOTER_ACCOUNT; ?></span></p>
<div class="vp-container">
<div>
<strong>VP:</strong> <span style="color: <?php echo $current_vp >= 80 ? 'var(--neon-green)' : ($current_vp >= 50 ? 'var(--neon-yellow)' : 'var(--neon-red)'); ?>;">
<?php echo $current_vp; ?>%
</span>
</div>
<div class="vp-bar">
<div class="vp-fill"></div>
</div>
</div>
<p><strong>Eligible Posts:</strong> <span style="color: var(--neon-green);"><?php echo count($eligible_posts); ?></span>/<?php echo count($posts); ?></p>
<!-- ========== AFFICHAGE DES RÈGLES DE VOTE ========== -->
<div style="margin-top: 20px; padding: 15px; background: rgba(0, 212, 255, 0.05); border-radius: 8px; border: 1px solid rgba(0, 212, 255, 0.2);">
<h3 style="color: var(--neon-blue); margin-bottom: 10px; font-size: 1.1rem;">⚙️ Vote Rules</h3>
<?php foreach ($VOTE_RULES as $min_words => $weight): ?>
<p style="margin: 5px 0; font-size: 0.95rem;">
<span style="color: var(--neon-yellow);">≥ <?php echo $min_words; ?> words</span>
→
<span style="color: var(--neon-green); font-weight: bold;"><?php echo $weight / 100; ?>%</span>
</p>
<?php endforeach; ?>
</div>
</div>
<?php if (count($eligible_posts) > 0): ?>
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">✅ Eligible Posts (<?php echo count($eligible_posts); ?>)</h2>
<button onclick="voteAll()" style="padding: 10px 20px;">🚀 Vote All</button>
</div>
<table>
<thead>
<tr>
<th>Author</th>
<th>Title</th>
<th>Words</th>
<th>Weight</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($eligible_posts as $p):
$words = count_words($p['body']);
$vote_weight = get_vote_weight($words);
?>
<tr>
<td>
<a href="https://peakd.com/@<?php echo $p['author']; ?>"
target="_blank"
style="color: var(--neon-blue); text-decoration: none;">
@<?php echo $p['author']; ?>
</a>
</td>
<td>
<a href="https://peakd.com/@<?php echo $p['author']; ?>/<?php echo $p['permlink']; ?>"
target="_blank"
style="color: var(--text-primary); text-decoration: none;">
<?php echo htmlspecialchars(mb_substr($p['title'], 0, 60)) . (mb_strlen($p['title']) > 60 ? '...' : ''); ?>
</a>
</td>
<td><?php echo $words; ?></td>
<td>
<span style="
color: <?php
echo $vote_weight >= 600 ? 'var(--neon-green)' :
($vote_weight >= 500 ? 'var(--neon-yellow)' : 'var(--neon-blue)');
?>;
font-weight: bold;
">
<?php echo $vote_weight / 100; ?>%
</span>
</td>
<td>
<button
class="vote-btn"
onclick="votePost(
'<?php echo addslashes($p['author']); ?>',
'<?php echo addslashes($p['permlink']); ?>',
this,
<?php echo $words; ?>
)">
Vote <?php echo $vote_weight / 100; ?>%
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card">
<h2>⚠️ No Eligible Posts</h2>
<p style="color: var(--text-secondary);">All posts have already been voted on or don't meet criteria.</p>
</div>
<?php endif; ?>
<!-- ========== PATCH 1: Vote History (AMÉLIORÉ) ========== -->
<div id="vote-history" style="
margin-top: 40px;
padding: 30px;
background: linear-gradient(135deg, rgba(18, 18, 48, 0.95), rgba(30, 30, 70, 0.85));
border-radius: 15px;
border: 2px solid var(--neon-purple);
box-shadow: 0 8px 32px rgba(178, 0, 255, 0.3);
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="
font-family: 'Orbitron', sans-serif;
color: var(--neon-purple);
text-shadow: 0 0 10px var(--neon-purple);
margin: 0;
">📊 Vote Session History</h2>
<button onclick="clearVoteHistory()" style="
background: linear-gradient(45deg, #ff0040, #ff6b9d);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
font-family: 'Orbitron', sans-serif;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
🗑️ Clear History
</button>
</div>
<div id="history-list" style="
display: grid;
gap: 15px;
max-height: 500px;
overflow-y: auto;
"></div>
</div>
<?php if (DEBUG_MODE): ?>
<div class="debug-table">
<h2>🐞 Debug Log</h2>
<table>
<thead>
<tr>
<th>Author</th>
<th>Permlink</th>
<th>Words</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($debug_logs as $log): ?>
<tr>
<td>@<?php echo htmlspecialchars($log['author']); ?></td>
<td><?php echo htmlspecialchars($log['permlink']); ?></td>
<td><?php echo $log['words']; ?></td>
<td><?php echo $log['reason']; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script>
// ========== CONFIGURATION ==========
const VOTE_STORAGE_KEY = 'hiveVoteHistory';
const ACCOUNT_STORAGE_KEY = 'hiveVoterAccount';
const VOTE_DELAY = 5000;
const AUTO_REFRESH_DELAY = 1800000;
// ========== AUTO-VOTE CONFIG (FROM PHP) ==========
const AUTO_VOTE_ENABLED = <?php echo AUTO_VOTE_ENABLED ? 'true' : 'false'; ?>;
const AUTO_VOTE_DELAY = <?php echo AUTO_VOTE_DELAY; ?>;
const AUTO_REFRESH_AFTER_VOTE = <?php echo AUTO_REFRESH_AFTER_VOTE ? 'true' : 'false'; ?>;
// ========== VOTE RULES (SYNCHRONIZED WITH PHP) ==========
const VOTE_RULES = <?php echo json_encode($VOTE_RULES); ?>;
function calculateVoteWeight(wordCount) {
// Trie les seuils par ordre décroissant
const sortedThresholds = Object.keys(VOTE_RULES)
.map(Number)
.sort((a, b) => b - a);
for (const minWords of sortedThresholds) {
if (wordCount >= minWords) {
return VOTE_RULES[minWords];
}
}
return 0;
}
// ========== VOTE SESSION ==========
let voteSession = {
history: [],
votedPosts: []
};
let autoRefreshTimer = null;
// ========== CONNECT ACCOUNT ==========
function connectAccount() {
const accountInput = document.getElementById('account-input');
const statusDiv = document.getElementById('account-status');
const username = accountInput.value.trim().replace('@', '');
if (!username) {
alert('⚠️ Please enter a username');
return;
}
statusDiv.style.display = 'block';
statusDiv.className = 'status-badge status-loading';
statusDiv.textContent = '⏳ Validating account...';
fetch('https://api.hive.blog', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'condenser_api.get_accounts',
params: [[username]],
id: 1
})
})
.then(res => res.json())
.then(data => {
if (data.result && data.result.length > 0) {
localStorage.setItem(ACCOUNT_STORAGE_KEY, username);
document.getElementById('current-account-display').textContent = `@${username}`;
statusDiv.className = 'status-badge status-success';
statusDiv.textContent = `✅ Connected as @${username}`;
console.log(`✅ Account validated: @${username}`);
} else {
statusDiv.className = 'status-badge status-failed';
statusDiv.textContent = '❌ Account not found';
}
})
.catch(err => {
statusDiv.className = 'status-badge status-failed';
statusDiv.textContent = '❌ Connection error';
console.error(err);
});
}
// ========== VOTE FUNCTION ==========
function votePost(author, permlink, button, wordCount) {
const savedAccount = localStorage.getItem(ACCOUNT_STORAGE_KEY) || '<?php echo VOTER_ACCOUNT; ?>';
if (button.disabled) return;
button.disabled = true;
button.textContent = "⏳ Voting...";
// Calculate weight using centralized function
let weight = calculateVoteWeight(wordCount);
console.log(`🗳️ Voting for @${author}/${permlink} with ${weight/100}% (${wordCount} words)`);
hive_keychain.requestVote(
savedAccount,
permlink,
author,
weight,
response => {
if (response.success) {
button.textContent = "✅ Voted";
button.classList.add("voted");
addVoteToSession(author, permlink, weight, true);
updateHistoryDisplay();
console.log(`✅ Successfully voted for @${author}/${permlink}`);
} else {
button.textContent = "❌ Failed";
button.style.background = "linear-gradient(90deg, var(--neon-red), #ff7a18)";
console.error("Vote error:", response.message);
alert(`❌ Vote failed: ${response.message}`);
}
}
);
}
// ========== ADD VOTE TO SESSION ==========
function addVoteToSession(author, permlink, weight, success) {
const voteData = {
author: author,
permlink: permlink,
weight: weight,
success: success,
timestamp: Date.now()
};
voteSession.history.unshift(voteData);
voteSession.votedPosts.push(`${author}/${permlink}`);
localStorage.setItem(VOTE_STORAGE_KEY, JSON.stringify(voteSession));
}
// ========== LOAD VOTE SESSION ==========
function loadVoteSession() {
const stored = localStorage.getItem(VOTE_STORAGE_KEY);
if (stored) {
voteSession = JSON.parse(stored);
updateHistoryDisplay();
}
}
// ========== CLEAR VOTE HISTORY ==========
function clearVoteHistory() {
if (confirm('🗑️ Clear all vote history?')) {
voteSession = { history: [], votedPosts: [] };
localStorage.removeItem(VOTE_STORAGE_KEY);
updateHistoryDisplay();
console.log('✅ Vote history cleared');
}
}
// ========== UPDATE HISTORY DISPLAY ==========
function updateHistoryDisplay() {
const historyList = document.getElementById('history-list');
if (voteSession.history.length === 0) {
historyList.innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">No votes yet in this session</p>';
return;
}
historyList.innerHTML = voteSession.history.map(vote => {
const authorName = `@${vote.author}`;
const postUrl = `https://peakd.com/@${vote.author}/${vote.permlink}`;
const profileUrl = `https://peakd.com/@${vote.author}`;
const avatarUrl = `https://images.hive.blog/u/${vote.author}/avatar`;
const timeAgo = getTimeAgo(vote.timestamp);
return `
<div class="history-item">
<a href="${profileUrl}" target="_blank">
<img src="${avatarUrl}" alt="${authorName}" class="history-avatar"
onerror="this.src='https://images.hive.blog/u/hive.blog/avatar'">
</a>
<div class="history-content">
<a href="${profileUrl}" target="_blank" class="history-author">
${authorName}
</a>
<a href="${postUrl}" target="_blank" class="history-post-link">
🔗 View post on PeakD
</a>
</div>
<div class="history-meta">
<span class="history-weight">💎 ${vote.weight / 100}%</span>
<span class="history-time">${timeAgo}</span>
</div>
</div>
`;
}).join('');
}
function getTimeAgo(timestamp) {
const now = Date.now();
const diffMs = now - timestamp;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
// ========== AUTO-VOTE FUNCTION ==========
async function startAutoVote() {
const username = localStorage.getItem(ACCOUNT_STORAGE_KEY)
|| '<?php echo VOTER_ACCOUNT; ?>';
// ✅ CHECK VP LIVE AU DÉMARRAGE
const res = await fetch('https://api.hive.blog', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'condenser_api.get_accounts',
params: [[username]],
id: 1
})
});
const data = await res.json();
const liveVP = data?.result?.[0]
? Math.round(data.result[0].voting_power / 100)
: 0;
console.log(`🔋 VP au démarrage : ${liveVP}%`);
if (liveVP < <?php echo MIN_VP_THRESHOLD; ?>) {
console.log("⛔ VP insuffisant, auto-vote annulé");
return;
}
// ✅ TON CODE AUTO-VOTE EXISTANT
const buttons = document.querySelectorAll("button.vote-btn:not([disabled])");
if (buttons.length === 0) {
console.log("✅ No posts to vote");
if (AUTO_REFRESH_AFTER_VOTE) scheduleAutoRefresh();
return;
}
console.log(`🤖 AUTO-VOTE: Starting for ${buttons.length} posts`);
showAutoVoteIndicator(buttons.length);
let index = 0;
function voteNext() {
if (index >= buttons.length) {
console.log("✅ AUTO-VOTE: Complete");
hideAutoVoteIndicator();
if (AUTO_REFRESH_AFTER_VOTE) scheduleAutoRefresh();
return;
}
updateAutoVoteIndicator(index + 1, buttons.length);
buttons[index].click();
index++;
setTimeout(voteNext, AUTO_VOTE_DELAY);
}
voteNext();
}
// ========== VOTE ALL FUNCTION (MANUAL) ==========
function voteAll() {
const buttons = document.querySelectorAll("button.vote-btn:not([disabled])");
if (buttons.length === 0) {
console.log("✅ No eligible posts - refreshing in 5 seconds");
scheduleAutoRefresh();
return;
}
let index = 0;
function voteNext() {
if (index >= buttons.length) {
console.log("✅ Voting complete - refreshing in 5 seconds");
scheduleAutoRefresh();
return;
}
buttons[index].click();
index++;
setTimeout(voteNext, 3000);
}
voteNext();
}
// ========== AUTO-VOTE INDICATOR ==========
function showAutoVoteIndicator(totalPosts) {
let indicator = document.getElementById('auto-vote-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'auto-vote-indicator';
indicator.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(18, 18, 48, 0.98);
border: 2px solid var(--neon-blue);
border-radius: 15px;
padding: 30px 40px;
z-index: 10000;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 212, 255, 0.5);
animation: pulse-glow 2s infinite;
`;
document.body.appendChild(indicator);
}
indicator.innerHTML = `
<div style="font-size: 3rem; margin-bottom: 15px;">🤖</div>
<div style="font-family: 'Orbitron', sans-serif; font-size: 1.5rem; color: var(--neon-blue); margin-bottom: 10px;">AUTO-VOTE ACTIVE</div>
<div id="auto-vote-progress" style="font-size: 1.2rem; color: var(--neon-green);">0 / ${totalPosts}</div>
`;
indicator.style.display = 'block';
}
function updateAutoVoteIndicator(current, total) {
const progress = document.getElementById('auto-vote-progress');
if (progress) {
progress.textContent = `${current} / ${total}`;
}
}
function hideAutoVoteIndicator() {
const indicator = document.getElementById('auto-vote-indicator');
if (indicator) {
indicator.style.animation = 'fadeOut 0.5s forwards';
setTimeout(() => {
indicator.remove();
}, 500);
}
}
// ========== AUTO-REFRESH FUNCTIONS ==========
function clearAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
const timerDisplay = document.getElementById('refresh-timer');
if (timerDisplay) {
timerDisplay.remove();
}
}
}
function scheduleAutoRefresh(delay = AUTO_REFRESH_DELAY) {
clearAutoRefresh();
let remainingTime = delay / 1000;
let timerDisplay = document.getElementById('refresh-timer');
if (!timerDisplay) {
timerDisplay = document.createElement('div');
timerDisplay.id = 'refresh-timer';
timerDisplay.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(18, 18, 48, 0.95);
border: 1px solid rgba(0, 212, 255, 0.4);
border-radius: 8px;
padding: 12px 20px;
color: var(--neon-blue);
font-family: 'Orbitron', sans-serif;
font-size: 0.9rem;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
animation: pulse-glow 2s infinite;
`;
document.body.appendChild(timerDisplay);
}
timerDisplay.textContent = `🔄 Refresh in ${remainingTime}s`;
autoRefreshTimer = setInterval(() => {
remainingTime--;
timerDisplay.textContent = `🔄 Refresh in ${remainingTime}s`;
if (remainingTime <= 0) {
clearAutoRefresh();
location.reload();
}
}, 1000);
}
// ========== EXPORT CSV ==========
function exportVotedAccountsCSV() {
if (voteSession.history.length === 0) {
alert('⚠️ No votes to export!');
return;
}
const uniqueAccounts = [...new Set(
voteSession.history.map(vote => vote.author.replace('@', ''))
)];
// ✅ HEADER OPTIONNEL (tu peux le supprimer si tu veux)
let csvContent = "";
uniqueAccounts.forEach(username => {
csvContent += `${username},0.01,GLYPH,You are eligible to vote\n`;
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
const filename = `glyph_voters_${timestamp}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log(`✅ Exported ${uniqueAccounts.length} accounts in GLYPH format`);
}
// ========== AUTO-START ON PAGE LOAD ==========
window.addEventListener('DOMContentLoaded', () => {
loadVoteSession();
const savedAccount = localStorage.getItem(ACCOUNT_STORAGE_KEY);
if (savedAccount) {
document.getElementById('current-account-display').textContent = `@${savedAccount}`;
}
const eligibleCount = <?php echo count($eligible_posts); ?>;
// Auto-vote si activé ET posts disponibles
if (AUTO_VOTE_ENABLED && eligibleCount > 0) {
console.log(`🤖 AUTO-VOTE enabled: ${eligibleCount} posts detected`);
setTimeout(() => {
startAutoVote();
}, 2000); // Délai de 2s pour laisser charger la page
} else if (eligibleCount === 0) {
console.log("⚠️ No eligible posts - scheduling auto-refresh");
scheduleAutoRefresh();
}
// Ajoute le bouton d'export CSV
const historySection = document.getElementById('vote-history');
if (historySection) {
const clearButton = historySection.querySelector('button[onclick="clearVoteHistory()"]');
if (clearButton) {
const exportButton = document.createElement('button');
exportButton.textContent = '📊 Export CSV';
exportButton.onclick = exportVotedAccountsCSV;
exportButton.style.cssText = `
background: linear-gradient(45deg, #00d4ff, #b200ff);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
margin-right: 10px;
transition: all 0.3s;
font-family: 'Orbitron', sans-serif;
`;
exportButton.onmouseover = () => exportButton.style.transform = 'scale(1.05)';
exportButton.onmouseout = () => exportButton.style.transform = 'scale(1)';
clearButton.parentElement.insertBefore(exportButton, clearButton);
}
}
});
window.addEventListener('beforeunload', () => {
clearAutoRefresh();
});
// Ajoute l'animation fadeOut pour l'indicateur
const style = document.createElement('style');
style.textContent = `
@keyframes fadeOut {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}
`;
document.head.appendChild(style);
</script>
<!-- ========== PATCH: Auto-Vote Play/Pause Control ========== -->
<div style="
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
background: rgba(18, 18, 48, 0.95);
border-radius: 50px;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 2px solid var(--neon-blue);
backdrop-filter: blur(10px);
">
<button
id="autoVoteToggle"
onclick="toggleAutoVote()"
style="
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple));
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.3s;
"
onmouseover="this.style.transform='scale(1.1)'"
onmouseout="this.style.transform='scale(1)'"
>
⏸
</button>
<div style="color: var(--neon-green); font-family: 'Orbitron', sans-serif;">
AUTO-VOTE: <span id="autoVoteStatus">ACTIVE</span>
</div>
</div>
<script>
// ========== AUTO-VOTE TOGGLE FUNCTION ==========
let autoVotePaused = false;
let autoVoteInterval = null;
function toggleAutoVote() {
autoVotePaused = !autoVotePaused;
const toggleBtn = document.getElementById('autoVoteToggle');
const statusText = document.getElementById('autoVoteStatus');
if (autoVotePaused) {
toggleBtn.textContent = "▶";
statusText.textContent = "PAUSED";
statusText.style.color = "var(--neon-red)";
clearAutoRefresh();
clearInterval(autoVoteInterval);
// Arrêter le processus auto-vote en cours
if (window.voteNextTimeout) {
clearTimeout(window.voteNextTimeout);
}
} else {
toggleBtn.textContent = "⏸";
statusText.textContent = "ACTIVE";
statusText.style.color = "var(--neon-green)";
// Relancer l'auto-vote si des posts sont éligibles
const eligibleCount = <?php echo count($eligible_posts); ?>;
if (eligibleCount > 0) {
setTimeout(() => {
startAutoVote();
}, 1000);
}
}
}
// Modification de startAutoVote pour gérer la pause
window.startAutoVoteOriginal = window.startAutoVote;
window.startAutoVote = async function() {
if (autoVotePaused) return;
await window.startAutoVoteOriginal();
}
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
// Vérifier si l'auto-vote est activé dans la config
if (!<?php echo AUTO_VOTE_ENABLED ? 'true' : 'false'; ?>) {
const toggleBtn = document.getElementById('autoVoteToggle');
const statusText = document.getElementById('autoVoteStatus');
toggleBtn.textContent = "▶";
statusText.textContent = "DISABLED";
statusText.style.color = "var(--text-secondary)";
toggleBtn.onclick = function() {
alert("Auto-vote is disabled in configuration. Set AUTO_VOTE_ENABLED to true in PHP config.");
};
}
});
</script>
<!-- ========== PATCH: Global Vote Counter ========== -->
<div id="global-vote-counter" style="
position: fixed;
bottom: 90px;
right: 20px;
z-index: 9999;
background: rgba(18, 18, 48, 0.95);
border-radius: 12px;
padding: 12px 18px;
border: 2px solid var(--neon-green);
color: var(--neon-green);
font-family: 'Orbitron', sans-serif;
font-size: 1rem;
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
">
🗳️ Votes: <span id="vote-count">0</span>
</div>
<script>
const VOTE_COUNT_KEY = 'global_vote_count';
// Charger le compteur au démarrage
function loadVoteCounter() {
const count = parseInt(localStorage.getItem(VOTE_COUNT_KEY)) || 0;
document.getElementById('vote-count').textContent = count;
}
// Incrémenter le compteur
function incrementVoteCounter() {
let count = parseInt(localStorage.getItem(VOTE_COUNT_KEY)) || 0;
count++;
localStorage.setItem(VOTE_COUNT_KEY, count);
document.getElementById('vote-count').textContent = count;
}
// Reset compteur
function resetVoteCounter() {
localStorage.removeItem(VOTE_COUNT_KEY);
document.getElementById('vote-count').textContent = 0;
}
// Hook dans le vote existant
const originalVotePost = votePost;
votePost = function(author, permlink, button, wordCount) {
originalVotePost(author, permlink, button, wordCount);
incrementVoteCounter();
};
// Hook dans le clear history
const originalClearHistory = clearVoteHistory;
clearVoteHistory = function() {
originalClearHistory();
resetVoteCounter();
};
// Init au chargement
document.addEventListener('DOMContentLoaded', loadVoteCounter);
</script>
<!-- ========== PATCH: Live Vote Weight Control ========== -->
<div id="vote-weight-control" style="
position: fixed;
bottom: 150px;
right: 20px;
z-index: 9999;
background: rgba(18, 18, 48, 0.95);
border-radius: 12px;
padding: 12px 18px;
border: 2px solid var(--neon-blue);
color: var(--neon-blue);
font-family: 'Orbitron', sans-serif;
font-size: 0.9rem;
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
">
⚙️ Vote % :
<input id="vote-weight-input" type="number" min="1" max="100" step="0.1" style="
width: 60px;
margin-left: 8px;
background: #111;
color: white;
border: 1px solid var(--neon-blue);
border-radius: 6px;
padding: 4px;
"> %
</div>
<script>
const VOTE_WEIGHT_KEY = 'custom_vote_weight';
// Charger la valeur
function loadVoteWeight() {
const saved = localStorage.getItem(VOTE_WEIGHT_KEY);
const input = document.getElementById('vote-weight-input');
if (saved) {
input.value = saved;
} else {
input.value = 5; // valeur par défaut
}
}
// Sauvegarde en live
document.addEventListener('input', function(e) {
if (e.target.id === 'vote-weight-input') {
localStorage.setItem(VOTE_WEIGHT_KEY, e.target.value);
}
});
// Override du calcul
const originalCalculateVoteWeight = calculateVoteWeight;
calculateVoteWeight = function(wordCount) {
const custom = localStorage.getItem(VOTE_WEIGHT_KEY);
if (custom) {
return Math.round(parseFloat(custom) * 100); // ex: 5 → 500
}
return originalCalculateVoteWeight(wordCount);
};
// Init
document.addEventListener('DOMContentLoaded', loadVoteWeight);
</script>
<!-- ========== PATCH: Live Vote Rules Editor ========== -->
<div id="vote-rules-editor" style="
position: fixed;
bottom: 220px;
right: 20px;
z-index: 9999;
background: rgba(18, 18, 48, 0.95);
border-radius: 12px;
padding: 15px;
border: 2px solid var(--neon-purple);
color: var(--neon-purple);
font-family: 'Orbitron', sans-serif;
font-size: 0.85rem;
box-shadow: 0 4px 15px rgba(178, 0, 255, 0.3);
width: 220px;
">
<div style="margin-bottom:10px;">⚙️ Rules Editor</div>
<div id="rules-container"></div>
<button onclick="addRule()" style="margin-top:10px;">➕ Add</button>
</div>
<script>
const RULES_KEY = 'custom_vote_rules';
// Charger règles
function loadRules() {
let rules = JSON.parse(localStorage.getItem(RULES_KEY));
if (!rules) {
rules = [{words:300, weight:5}];
localStorage.setItem(RULES_KEY, JSON.stringify(rules));
}
renderRules(rules);
}
// Affichage
function renderRules(rules) {
const container = document.getElementById('rules-container');
container.innerHTML = '';
rules.forEach((rule, index) => {
container.innerHTML += `
<div style="margin-bottom:6px;">
≥ <input type="number" value="${rule.words}" style="width:50px;" onchange="updateRule(${index}, 'words', this.value)">
→ <input type="number" value="${rule.weight}" style="width:40px;" onchange="updateRule(${index}, 'weight', this.value)"> %
<span onclick="deleteRule(${index})" style="cursor:pointer;color:red;">✖</span>
</div>
`;
});
}
// Update règle
function updateRule(index, key, value) {
let rules = JSON.parse(localStorage.getItem(RULES_KEY));
rules[index][key] = parseFloat(value);
localStorage.setItem(RULES_KEY, JSON.stringify(rules));
}
// Ajouter
function addRule() {
let rules = JSON.parse(localStorage.getItem(RULES_KEY));
rules.push({words:500, weight:10});
localStorage.setItem(RULES_KEY, JSON.stringify(rules));
renderRules(rules);
}
// Supprimer
function deleteRule(index) {
let rules = JSON.parse(localStorage.getItem(RULES_KEY));
rules.splice(index, 1);
localStorage.setItem(RULES_KEY, JSON.stringify(rules));
renderRules(rules);
}
// Override du calcul
const originalCalculateVoteWeightRules = calculateVoteWeight;
calculateVoteWeight = function(wordCount) {
const rules = JSON.parse(localStorage.getItem(RULES_KEY));
if (!rules) return originalCalculateVoteWeightRules(wordCount);
// tri décroissant
rules.sort((a,b) => b.words - a.words);
for (let rule of rules) {
if (wordCount >= rule.words) {
return Math.round(rule.weight * 100);
}
}
return 0;
};
// Update affichage (Rules + tableau)
function refreshUIRules() {
const rules = JSON.parse(localStorage.getItem(RULES_KEY));
if (!rules) return;
// bloc rules
const rulesDiv = document.querySelector('.card div[style*="Vote Rules"]');
if (rulesDiv) {
rulesDiv.innerHTML = `<h3 style="color: var(--neon-blue);">⚙️ Vote Rules (Live)</h3>` +
rules.map(r => `
<p style="margin:5px 0;">
<span style="color: var(--neon-yellow);">≥ ${r.words} words</span>
→
<span style="color: var(--neon-green); font-weight:bold;">${r.weight}%</span>
</p>
`).join('');
}
// tableau
document.querySelectorAll('td span').forEach(el => {
if (el.textContent.includes('%')) {
const rulesSorted = [...rules].sort((a,b)=>b.words-a.words);
const row = el.closest('tr');
const words = parseInt(row.children[2]?.textContent || 0);
for (let r of rulesSorted) {
if (words >= r.words) {
el.textContent = r.weight + '%';
break;
}
}
}
});
}
// refresh après modif
document.addEventListener('input', function(e){
if (e.target.closest('#rules-container')) {
refreshUIRules();
}
});
// init
document.addEventListener('DOMContentLoaded', () => {
loadRules();
refreshUIRules();
});
</script>
<script>
// Clé de contrôle
const AUTO_VOTE_STATE_KEY = 'auto_vote_state';
// Force état PAUSE au premier load
document.addEventListener('DOMContentLoaded', () => {
let state = localStorage.getItem(AUTO_VOTE_STATE_KEY);
// Si jamais pas encore défini → on force PAUSE
if (!state) {
localStorage.setItem(AUTO_VOTE_STATE_KEY, 'paused');
state = 'paused';
}
applyAutoVoteState(state);
});
// Appliquer état UI + logique
function applyAutoVoteState(state) {
const btn = document.querySelector('#auto-vote-button'); // adapte si ton ID est différent
if (!btn) return;
if (state === 'paused') {
btn.innerHTML = '▶️ START AUTO-VOTE';
btn.style.background = 'linear-gradient(45deg, #00c6ff, #0072ff)';
btn.dataset.state = 'paused';
// stop ton loop si existant
if (window.autoVoteInterval) {
clearInterval(window.autoVoteInterval);
window.autoVoteInterval = null;
}
console.log('⏸️ Auto-vote paused');
} else {
btn.innerHTML = '⏸️ PAUSE AUTO-VOTE';
btn.style.background = 'linear-gradient(45deg, #ff4d4d, #ff0000)';
btn.dataset.state = 'running';
startAutoVote(); // utilise ta fonction existante
console.log('▶️ Auto-vote running');
}
}
// Toggle bouton
document.addEventListener('click', function(e) {
if (e.target.closest('#auto-vote-button')) {
let current = localStorage.getItem(AUTO_VOTE_STATE_KEY) || 'paused';
let next = current === 'running' ? 'paused' : 'running';
localStorage.setItem(AUTO_VOTE_STATE_KEY, next);
applyAutoVoteState(next);
}
});
</script>
<!-- ========== PATCH: VP LIMIT CONTROL ========== -->
<div id="vp-control" style="
position: fixed;
bottom: 450px;
right: 20px;
z-index: 9999;
background: rgba(18, 18, 48, 0.95);
border-radius: 12px;
padding: 12px 15px;
border: 2px solid var(--neon-yellow);
color: var(--neon-yellow);
font-family: 'Orbitron', sans-serif;
font-size: 0.85rem;
box-shadow: 0 4px 15px rgba(255, 255, 0, 0.3);
width: 200px;
">
<div>⚡ Min VP</div>
<input id="vp-threshold" type="number" min="1" max="100" style="width:60px; margin-top:8px;">
<span>%</span>
</div>
<script>
const VP_THRESHOLD_KEY = 'vp_threshold';
// init
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('vp-threshold');
let saved = localStorage.getItem(VP_THRESHOLD_KEY);
if (!saved) {
saved = 80; // valeur par défaut
localStorage.setItem(VP_THRESHOLD_KEY, saved);
}
input.value = saved;
});
// save live
document.addEventListener('input', function(e){
if (e.target.id === 'vp-threshold') {
localStorage.setItem(VP_THRESHOLD_KEY, e.target.value);
}
});
// check VP avant vote
function canVoteWithVP(currentVP) {
const threshold = parseFloat(localStorage.getItem(VP_THRESHOLD_KEY) || 80);
return currentVP >= threshold;
}
</script>
<!-- ========== PATCH: No Alert / Auto-Skip Errors ========== -->
<script>
// ✅ 1. Neutraliser TOUS les alert() (anti blocage total)
window.alert = function(message) {
console.warn("🚫 Alert bloquée :", message);
};
// ✅ 2. Optionnel : petit toast visuel (non bloquant)
function showToast(msg) {
const div = document.createElement('div');
div.textContent = msg;
div.style = `
position: fixed;
bottom: 20px;
left: 20px;
background: #ff2e63;
color: white;
padding: 10px 15px;
border-radius: 8px;
z-index: 9999;
font-size: 0.8rem;
opacity: 0.9;
font-family: sans-serif;
`;
document.body.appendChild(div);
setTimeout(() => div.remove(), 3000);
}
// ✅ 3. Hook automatique sur les boutons de vote
document.addEventListener('click', function(e) {
const btn = e.target.closest('button');
if (!btn) return;
// détecte les boutons de vote (tu peux ajuster si besoin)
if (btn.textContent.includes('%') || btn.textContent.includes('Vote')) {
// sécurité anti freeze visuel
setTimeout(() => {
if (btn.textContent.includes('❌')) {
console.log("⚠️ Vote failed auto-skip");
showToast("Vote failed → skipped");
}
}, 1500);
}
});
// ✅ 4. Sécurité globale JS (aucune erreur ne bloque)
window.onerror = function(msg, url, line, col, error) {
console.error("💥 JS Error caught:", msg);
return true; // empêche blocage
};
window.onunhandledrejection = function(event) {
console.error("💥 Promise Error:", event.reason);
};
</script>
<!-- ===== 💝 BIG DONATION BUTTON TOP LEFT ===== -->
<div style="
position: fixed;
top: 20px;
left: 20px;
z-index: 10000;
">
<button onclick="openDonationModal()" style="
background: linear-gradient(135deg, #ff6b9d, #ffa800);
color: white;
border: none;
border-radius: 16px;
padding: 18px 28px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 8px 30px rgba(255, 107, 157, 0.6);
transition: all 0.25s ease;
max-width: 280px;
text-align: left;
line-height: 1.4;
animation: pulse 2s infinite;
"
onmouseover="this.style.transform='scale(1.08)'"
onmouseout="this.style.transform='scale(1)'">
💝 If you want to support the project,<br>
I accept donations.
</button>
</div>
<!-- ===== 💎 DONATION MODAL ===== -->
<div id="donationModal" style="
display:none;
position: fixed;
top:0; left:0;
width:100%; height:100%;
background: rgba(0,0,0,0.7);
z-index:10001;
justify-content:center;
align-items:center;
">
<div style="
background:#111;
padding:25px;
border-radius:12px;
width:320px;
color:white;
text-align:center;
box-shadow: 0 0 25px rgba(255,107,157,0.4);
">
<h2>💝 Support the project</h2>
<input id="donationUser" placeholder="Your Hive username" style="width:100%; padding:10px; margin:10px 0; border-radius:8px; border:none;">
<input id="donationAmount" placeholder="Amount" type="number" style="width:100%; padding:10px; margin:10px 0; border-radius:8px; border:none;">
<select id="donationToken" style="width:100%; padding:10px; margin:10px 0; border-radius:8px;">
<option value="HIVE">HIVE</option>
<option value="HBD">HBD</option>
<option value="ALIVE">ALIVE</option>
</select>
<button onclick="sendDonation()" style="
width:100%;
padding:12px;
margin-top:10px;
background:#ff6b9d;
border:none;
border-radius:8px;
color:white;
font-weight:bold;
cursor:pointer;
">
Send Donation
</button>
<button onclick="closeDonationModal()" style="
width:100%;
padding:10px;
margin-top:10px;
background:#333;
border:none;
border-radius:8px;
color:white;
cursor:pointer;
">
Cancel
</button>
</div>
</div>
<!-- ===== ⚙️ STYLE ANIMATION ===== -->
<style>
@keyframes pulse {
0% { box-shadow: 0 0 0 rgba(255,107,157,0.6); }
50% { box-shadow: 0 0 30px rgba(255,107,157,1); }
100% { box-shadow: 0 0 0 rgba(255,107,157,0.6); }
}
</style>
<!-- ===== ⚙️ SCRIPT ===== -->
<script>
function openDonationModal() {
document.getElementById('donationModal').style.display = 'flex';
}
function closeDonationModal() {
document.getElementById('donationModal').style.display = 'none';
}
function sendDonation() {
const user = document.getElementById('donationUser').value;
const amount = document.getElementById('donationAmount').value;
const token = document.getElementById('donationToken').value;
if (!user || !amount) {
alert("Please fill all fields");
return;
}
const to = "we-are-ai";
const memo = "Donation ❤️";
if (token === "HIVE") {
window.hive_keychain.requestTransfer(
user,
to,
amount,
memo,
"HIVE",
function(response) {
alert("✅ Donation sent!");
closeDonationModal();
}
);
} else {
window.hive_keychain.requestCustomJson(
user,
"ssc-mainnet-hive",
"Active",
JSON.stringify({
contractName: "tokens",
contractAction: "transfer",
contractPayload: {
symbol: token,
to: to,
quantity: amount,
memo: memo
}
}),
"Hive-Engine transfer",
function(response) {
alert("✅ Donation sent!");
closeDonationModal();
}
);
}
}
</script>
</body>
</html>