feat: Implement real data fetching for points chart and enhance UI with config editor and history viewer styles

This commit is contained in:
2026-01-02 18:12:19 +01:00
parent 57e98e6f03
commit 7a483fd139
8 changed files with 256 additions and 54 deletions

View File

@@ -142,7 +142,7 @@ function initPointsChart() {
labels: generateDateLabels(7),
datasets: [{
label: 'Points',
data: generatePlaceholderData(7),
data: new Array(7).fill(0), // Real data loaded from API
borderColor: '#58a6ff',
backgroundColor: gradient,
borderWidth: 2,
@@ -203,16 +203,6 @@ function generateDateLabels(days) {
return labels
}
function generatePlaceholderData(count) {
const data = []
let base = 150
for (let i = 0; i < count; i++) {
base += Math.floor(Math.random() * 50) - 20
data.push(Math.max(50, base))
}
return data
}
function setChartPeriod(period, btn) {
document.querySelectorAll('.period-btn').forEach((b) => { b.classList.remove('active') })
btn.classList.add('active')
@@ -220,11 +210,58 @@ function setChartPeriod(period, btn) {
const days = period === '7d' ? 7 : 30
if (pointsChart) {
pointsChart.data.labels = generateDateLabels(days)
pointsChart.data.datasets[0].data = generatePlaceholderData(days)
pointsChart.update('none')
// Fetch actual data from account history
fetch('/api/account-history')
.then(r => r.json())
.then(history => {
const pointsData = extractPointsFromHistory(history, days)
pointsChart.data.datasets[0].data = pointsData
pointsChart.update('none')
})
.catch(() => {
// Fallback to zeros if API fails
pointsChart.data.datasets[0].data = new Array(days).fill(0)
pointsChart.update('none')
})
}
}
function extractPointsFromHistory(history, days) {
const dataByDate = {}
const today = new Date()
// Initialize all days with 0
for (let i = 0; i < days; i++) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const key = d.toISOString().slice(0, 10)
dataByDate[key] = 0
}
// Fill with actual data
for (const email in history) {
const accountHistory = history[email]
for (const day in accountHistory) {
if (dataByDate.hasOwnProperty(day)) {
const dayData = accountHistory[day]
dataByDate[day] += dayData.pointsEarnedToday || 0
}
}
}
// Convert to array (reverse chronological)
const result = []
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const key = d.toISOString().slice(0, 10)
result.push(dataByDate[key] || 0)
}
return result
}
// Data Loading
function loadInitialData() {
fetch('/api/status').then((r) => { return r.json() }).then(updateBotStatus).catch(() => { })
@@ -298,7 +335,7 @@ function updateChartsWithRealData(histories) {
var data = sortedDates.map((d) => { return last7Days[d] })
pointsChart.data.labels = labels.length > 0 ? labels : generateDateLabels(7)
pointsChart.data.datasets[0].data = data.length > 0 ? data : generatePlaceholderData(7)
pointsChart.data.datasets[0].data = data.length > 0 ? data : new Array(7).fill(0)
pointsChart.update()
}
@@ -675,11 +712,95 @@ function exportLogs() {
}
function openConfig() {
showToast('Config editor coming soon', 'info')
showModal('Configuration Editor', `
<div class="config-loading">Loading configuration...</div>
`, [])
// Fetch current config
fetch('/api/config')
.then(r => r.json())
.then(config => {
const body = `
<div class="config-editor">
<textarea id="configEditor" class="config-textarea">${JSON.stringify(config, null, 2)}</textarea>
<p class="config-hint">⚠️ Advanced users only. Invalid JSON will break the bot.</p>
</div>
`
const buttons = [
{ cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Cancel' },
{ cls: 'btn btn-sm btn-primary', action: 'saveConfig()', text: 'Save Changes' }
]
showModal('Configuration Editor', body, buttons)
})
.catch(e => {
showToast('Failed to load config: ' + e.message, 'error')
closeModal()
})
}
function saveConfig() {
const editor = document.getElementById('configEditor')
if (!editor) return
try {
const newConfig = JSON.parse(editor.value)
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showToast('Configuration saved! Restart bot for changes to apply.', 'success')
closeModal()
} else {
showToast('Save failed: ' + (data.error || 'Unknown error'), 'error')
}
})
.catch(e => {
showToast('Save failed: ' + e.message, 'error')
})
} catch (e) {
showToast('Invalid JSON format: ' + e.message, 'error')
}
}
function viewHistory() {
showToast('History viewer coming soon', 'info')
showModal('Run History', '<div class="config-loading">Loading history...</div>', [])
fetch('/api/history')
.then(r => r.json())
.then(history => {
if (!history || history.length === 0) {
showModal('Run History', '<p class="log-empty">No history available yet</p>', [
{ cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Close' }
])
return
}
const rows = history.slice(0, 10).map(h => `
<div class="history-row">
<div class="history-date">${new Date(h.timestamp || Date.now()).toLocaleString()}</div>
<div class="history-stats">
<span>✅ ${h.successCount || 0} success</span>
<span>❌ ${h.errorCount || 0} errors</span>
<span>🎯 ${h.totalPoints || 0} pts</span>
</div>
</div>
`).join('')
const body = `<div class="history-list">${rows}</div>`
const buttons = [
{ cls: 'btn btn-sm btn-secondary', action: 'closeModal()', text: 'Close' }
]
showModal('Run History (Last 10 Runs)', body, buttons)
})
.catch(e => {
showToast('Failed to load history: ' + e.message, 'error')
closeModal()
})
}
// UI Utilities

View File

@@ -8,6 +8,7 @@
<link rel="icon" type="image/svg+xml"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E🎯%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/style-extensions.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
/* Inline SVG icon styles */

View File

@@ -0,0 +1,82 @@
/* Dashboard Extensions - Config Editor & History Viewer */
/* Config Editor Styles */
.config-editor {
width: 100%;
min-height: 400px;
}
.config-textarea {
width: 100%;
min-height: 400px;
padding: var(--spacing-md);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
resize: vertical;
transition: border-color var(--transition-fast);
}
.config-textarea:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
}
.config-hint {
margin-top: var(--spacing-sm);
font-size: 13px;
color: var(--text-muted);
text-align: center;
}
.config-loading {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
/* History Viewer Styles */
.history-list {
max-height: 500px;
overflow-y: auto;
}
.history-row {
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border-left: 3px solid var(--accent-blue);
transition: all var(--transition-fast);
}
.history-row:hover {
background: var(--bg-secondary);
border-left-color: var(--accent-green);
transform: translateX(2px);
}
.history-date {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.history-stats {
display: flex;
gap: var(--spacing-md);
font-size: 13px;
color: var(--text-secondary);
}
.history-stats span {
padding: 2px 8px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
}

View File

@@ -49,17 +49,17 @@
/* Light Theme */
.light-theme {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--bg-tertiary: #edf1f5;
--bg-card: #ffffff;
--border-color: #d0d7de;
--text-primary: #1f2328;
--text-secondary: #656d76;
--text-muted: #8c959f;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.2);
--border-color: #d8dee4;
--text-primary: #1a202c;
--text-secondary: #4a5568;
--text-muted: #718096;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 12px 30px rgba(0, 0, 0, 0.15);
}
/* Reset & Base */