|
|
|
|
|
class Dashboard { |
|
|
constructor() { |
|
|
this.widgets = []; |
|
|
this.intervals = {}; |
|
|
this.modal = new bootstrap.Modal(document.getElementById('widgetModal')); |
|
|
this.init(); |
|
|
} |
|
|
|
|
|
async init() { |
|
|
|
|
|
this.updateDateTime(); |
|
|
setInterval(() => this.updateDateTime(), 1000); |
|
|
|
|
|
|
|
|
await this.loadWidgets(); |
|
|
|
|
|
|
|
|
this.renderWidgets(); |
|
|
|
|
|
|
|
|
document.getElementById('last-update').textContent = new Date().toLocaleTimeString(); |
|
|
} |
|
|
|
|
|
async loadWidgets() { |
|
|
try { |
|
|
const response = await fetch('widgets.json'); |
|
|
this.widgets = await response.json(); |
|
|
console.log('Widgets loaded successfully:', this.widgets); |
|
|
} catch (error) { |
|
|
console.error('Error loading widgets:', error); |
|
|
|
|
|
this.widgets = [ |
|
|
{ |
|
|
id: 'default-metric', |
|
|
type: 'metric', |
|
|
title: 'Default Metric', |
|
|
size: 'small', |
|
|
color: 'blue', |
|
|
refreshInterval: 5000, |
|
|
format: 'number', |
|
|
historyLabel: '24h', |
|
|
currentValue: 1000, |
|
|
history: Array(7).fill(0).map((_, i) => 900 + i * 20) |
|
|
}, |
|
|
{ |
|
|
id: 'default-uptime', |
|
|
type: 'uptime', |
|
|
title: 'Default Uptime', |
|
|
size: 'medium', |
|
|
color: 'green', |
|
|
refreshInterval: 10000, |
|
|
format: 'status', |
|
|
historyLabel: '7d', |
|
|
currentValue: 'online', |
|
|
latency: 100, |
|
|
uptime: 0.999, |
|
|
history: Array(7).fill(1) |
|
|
}, |
|
|
{ |
|
|
id: 'default-news', |
|
|
type: 'news', |
|
|
title: 'Default News', |
|
|
size: 'large', |
|
|
color: 'yellow', |
|
|
refreshInterval: 30000, |
|
|
format: 'list', |
|
|
historyLabel: '', |
|
|
items: ['News item 1', 'News item 2', 'News item 3'] |
|
|
} |
|
|
]; |
|
|
} |
|
|
} |
|
|
|
|
|
renderWidgets() { |
|
|
const container = document.getElementById('widgets-container'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
this.widgets.forEach(widget => { |
|
|
const widgetElement = this.createWidgetElement(widget); |
|
|
container.appendChild(widgetElement); |
|
|
|
|
|
|
|
|
this.setupWidgetRefresh(widget); |
|
|
}); |
|
|
} |
|
|
|
|
|
createWidgetElement(widget) { |
|
|
|
|
|
const col = document.createElement('div'); |
|
|
let colClass = 'col-md-4 col-lg-4'; |
|
|
|
|
|
if (widget.size === 'medium') { |
|
|
colClass = 'col-md-6 col-lg-6'; |
|
|
} else if (widget.size === 'large') { |
|
|
colClass = 'col-md-12 col-lg-12'; |
|
|
} |
|
|
|
|
|
col.className = colClass; |
|
|
|
|
|
|
|
|
const card = document.createElement('div'); |
|
|
card.className = `widget-card ${widget.size} widget-bg-${widget.color}`; |
|
|
card.dataset.widgetId = widget.id; |
|
|
card.style.cursor = 'pointer'; |
|
|
|
|
|
|
|
|
card.addEventListener('click', () => this.openWidgetModal(widget)); |
|
|
|
|
|
|
|
|
const header = document.createElement('div'); |
|
|
header.className = 'widget-header'; |
|
|
|
|
|
const title = document.createElement('h3'); |
|
|
title.className = 'widget-title'; |
|
|
title.innerHTML = ` |
|
|
<span class="badge-color" style="background-color: var(--bs-${widget.color})"></span> |
|
|
${widget.title} |
|
|
`; |
|
|
|
|
|
const refreshBadge = document.createElement('span'); |
|
|
refreshBadge.className = 'badge bg-secondary'; |
|
|
refreshBadge.textContent = `Every ${widget.refreshInterval / 1000}s`; |
|
|
|
|
|
header.appendChild(title); |
|
|
header.appendChild(refreshBadge); |
|
|
|
|
|
|
|
|
const body = document.createElement('div'); |
|
|
body.className = 'widget-body'; |
|
|
|
|
|
if (widget.type === 'metric') { |
|
|
this.renderMetricWidget(body, widget); |
|
|
} else if (widget.type === 'uptime') { |
|
|
this.renderUptimeWidget(body, widget); |
|
|
} else if (widget.type === 'news') { |
|
|
this.renderNewsWidget(body, widget); |
|
|
} |
|
|
|
|
|
|
|
|
card.appendChild(header); |
|
|
card.appendChild(body); |
|
|
|
|
|
|
|
|
col.appendChild(card); |
|
|
|
|
|
return col; |
|
|
} |
|
|
|
|
|
renderMetricWidget(container, widget) { |
|
|
|
|
|
let formattedValue; |
|
|
if (widget.format === 'currency') { |
|
|
formattedValue = new Intl.NumberFormat('en-US', { |
|
|
style: 'currency', |
|
|
currency: 'USD', |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
}).format(widget.currentValue); |
|
|
} else if (widget.format === 'percent') { |
|
|
formattedValue = new Intl.NumberFormat('en-US', { |
|
|
style: 'percent', |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
}).format(widget.currentValue); |
|
|
} else { |
|
|
formattedValue = new Intl.NumberFormat('en-US').format(widget.currentValue); |
|
|
} |
|
|
|
|
|
|
|
|
const change = ((widget.history[widget.history.length - 1] - widget.history[0]) / widget.history[0]) * 100; |
|
|
const changeFormatted = new Intl.NumberFormat('en-US', { |
|
|
style: 'percent', |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2, |
|
|
signDisplay: 'always' |
|
|
}).format(change / 100); |
|
|
|
|
|
|
|
|
const valueDisplay = document.createElement('div'); |
|
|
valueDisplay.className = 'value-display'; |
|
|
valueDisplay.textContent = formattedValue; |
|
|
|
|
|
|
|
|
const changeIndicator = document.createElement('div'); |
|
|
changeIndicator.className = 'value-change'; |
|
|
changeIndicator.innerHTML = ` |
|
|
${changeFormatted} <span class="ms-1">${widget.historyLabel}</span> |
|
|
`; |
|
|
changeIndicator.style.color = change >= 0 ? '#28a745' : '#dc3545'; |
|
|
|
|
|
|
|
|
const chartContainer = document.createElement('div'); |
|
|
chartContainer.className = 'chart-container'; |
|
|
const canvas = document.createElement('canvas'); |
|
|
chartContainer.appendChild(canvas); |
|
|
|
|
|
|
|
|
container.appendChild(valueDisplay); |
|
|
container.appendChild(changeIndicator); |
|
|
container.appendChild(chartContainer); |
|
|
|
|
|
|
|
|
this.renderSparkline(canvas, widget); |
|
|
} |
|
|
|
|
|
renderUptimeWidget(container, widget) { |
|
|
|
|
|
const statusBadge = document.createElement('span'); |
|
|
statusBadge.className = `status-badge status-${widget.currentValue}`; |
|
|
statusBadge.textContent = widget.currentValue.toUpperCase(); |
|
|
|
|
|
|
|
|
const statsRow = document.createElement('div'); |
|
|
statsRow.className = 'uptime-stats'; |
|
|
|
|
|
|
|
|
const uptimeStat = document.createElement('div'); |
|
|
uptimeStat.className = 'uptime-stat'; |
|
|
uptimeStat.innerHTML = ` |
|
|
<div class="uptime-value">${(widget.uptime * 100).toFixed(2)}%</div> |
|
|
<div class="uptime-label">UPTIME</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const latencyStat = document.createElement('div'); |
|
|
latencyStat.className = 'uptime-stat'; |
|
|
latencyStat.innerHTML = ` |
|
|
<div class="uptime-value">${widget.latency}ms</div> |
|
|
<div class="uptime-label">LATENCY</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const historyStat = document.createElement('div'); |
|
|
historyStat.className = 'uptime-stat'; |
|
|
historyStat.innerHTML = ` |
|
|
<div class="uptime-value">${widget.historyLabel}</div> |
|
|
<div class="uptime-label">HISTORY</div> |
|
|
`; |
|
|
|
|
|
|
|
|
statsRow.appendChild(uptimeStat); |
|
|
statsRow.appendChild(latencyStat); |
|
|
statsRow.appendChild(historyStat); |
|
|
|
|
|
|
|
|
const chartContainer = document.createElement('div'); |
|
|
chartContainer.className = 'chart-container mt-3'; |
|
|
const canvas = document.createElement('canvas'); |
|
|
chartContainer.appendChild(canvas); |
|
|
|
|
|
|
|
|
container.appendChild(statusBadge); |
|
|
container.appendChild(statsRow); |
|
|
container.appendChild(chartContainer); |
|
|
|
|
|
|
|
|
this.renderUptimeChart(canvas, widget); |
|
|
} |
|
|
|
|
|
renderNewsWidget(container, widget) { |
|
|
|
|
|
const list = document.createElement('div'); |
|
|
|
|
|
widget.items.forEach(item => { |
|
|
const newsItem = document.createElement('div'); |
|
|
newsItem.className = 'news-item'; |
|
|
newsItem.textContent = item; |
|
|
list.appendChild(newsItem); |
|
|
}); |
|
|
|
|
|
|
|
|
const refreshInfo = document.createElement('div'); |
|
|
refreshInfo.className = 'text-muted mt-2 text-end'; |
|
|
refreshInfo.style.fontSize = '0.8rem'; |
|
|
refreshInfo.textContent = `Updated ${new Date().toLocaleTimeString()}`; |
|
|
|
|
|
container.appendChild(list); |
|
|
container.appendChild(refreshInfo); |
|
|
} |
|
|
|
|
|
renderSparkline(canvas, widget) { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: widget.history.map((_, i) => ''), |
|
|
datasets: [{ |
|
|
data: widget.history, |
|
|
borderColor: `var(--bs-${widget.color})`, |
|
|
borderWidth: 2, |
|
|
fill: false, |
|
|
tension: 0.4, |
|
|
pointRadius: 0 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: false }, |
|
|
tooltip: { enabled: false } |
|
|
}, |
|
|
scales: { |
|
|
x: { display: false }, |
|
|
y: { display: false } |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
renderUptimeChart(canvas, widget) { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const labels = widget.history.map((_, i) => ''); |
|
|
const data = widget.history.map(val => val === 1 ? 100 : 0); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: labels, |
|
|
datasets: [{ |
|
|
data: data, |
|
|
borderColor: `var(--bs-${widget.color})`, |
|
|
backgroundColor: `rgba(var(--bs-${widget.color}-rgb), 0.1)`, |
|
|
borderWidth: 2, |
|
|
fill: true, |
|
|
tension: 0.1, |
|
|
pointRadius: 0 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: false }, |
|
|
tooltip: { enabled: false } |
|
|
}, |
|
|
scales: { |
|
|
x: { display: false }, |
|
|
y: { |
|
|
display: false, |
|
|
min: 0, |
|
|
max: 100 |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
setupWidgetRefresh(widget) { |
|
|
|
|
|
if (this.intervals[widget.id]) { |
|
|
clearInterval(this.intervals[widget.id]); |
|
|
} |
|
|
|
|
|
|
|
|
this.intervals[widget.id] = setInterval(() => { |
|
|
this.updateWidgetData(widget); |
|
|
}, widget.refreshInterval); |
|
|
} |
|
|
|
|
|
updateWidgetData(widget) { |
|
|
|
|
|
const updatedWidget = this.generateNewData(widget); |
|
|
|
|
|
|
|
|
const widgetElement = document.querySelector(`[data-widget-id="${widget.id}"]`); |
|
|
if (!widgetElement) return; |
|
|
|
|
|
|
|
|
const body = widgetElement.querySelector('.widget-body'); |
|
|
body.innerHTML = ''; |
|
|
|
|
|
if (widget.type === 'metric') { |
|
|
this.renderMetricWidget(body, updatedWidget); |
|
|
} else if (widget.type === 'uptime') { |
|
|
this.renderUptimeWidget(body, updatedWidget); |
|
|
} else if (widget.type === 'news') { |
|
|
this.renderNewsWidget(body, updatedWidget); |
|
|
} |
|
|
|
|
|
|
|
|
const index = this.widgets.findIndex(w => w.id === widget.id); |
|
|
if (index !== -1) { |
|
|
this.widgets[index] = updatedWidget; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('last-update').textContent = new Date().toLocaleTimeString(); |
|
|
} |
|
|
|
|
|
generateNewData(widget) { |
|
|
const now = new Date(); |
|
|
const clone = JSON.parse(JSON.stringify(widget)); |
|
|
|
|
|
if (clone.type === 'metric') { |
|
|
|
|
|
const changeFactor = 1 + (Math.random() * 0.1 - 0.05); |
|
|
clone.currentValue = clone.currentValue * changeFactor; |
|
|
|
|
|
|
|
|
clone.history.shift(); |
|
|
clone.history.push(clone.currentValue); |
|
|
|
|
|
} else if (clone.type === 'uptime') { |
|
|
|
|
|
if (Math.random() < 0.01) { |
|
|
clone.currentValue = clone.currentValue === 'online' ? 'offline' : 'online'; |
|
|
} |
|
|
|
|
|
|
|
|
clone.latency = Math.max(10, clone.latency + (Math.random() * 40 - 20)); |
|
|
|
|
|
|
|
|
clone.history.shift(); |
|
|
clone.history.push(clone.currentValue === 'online' ? 1 : 0); |
|
|
|
|
|
|
|
|
const uptimeDays = clone.history.filter(val => val === 1).length; |
|
|
clone.uptime = uptimeDays / clone.history.length; |
|
|
|
|
|
} else if (clone.type === 'news') { |
|
|
|
|
|
clone.items.shift(); |
|
|
|
|
|
|
|
|
const newsTemplates = [ |
|
|
'New regulation affects ${sector} market', |
|
|
'${company} announces partnership with ${partner}', |
|
|
'${coin} price surges ${percent}% in ${time}', |
|
|
'Industry experts predict ${trend} for ${sector}', |
|
|
'${company} reports ${percent}% ${change} in revenue', |
|
|
'Market analysts see ${trend} in ${sector} sector' |
|
|
]; |
|
|
|
|
|
const sectors = ['crypto', 'finance', 'tech', 'AI', 'blockchain', 'DeFi']; |
|
|
const companies = ['Binance', 'Coinbase', 'FTX', 'Kraken', 'MicroStrategy', 'Tesla']; |
|
|
const coins = ['BTC', 'ETH', 'SOL', 'ADA', 'DOT', 'AVAX']; |
|
|
|
|
|
const template = newsTemplates[Math.floor(Math.random() * newsTemplates.length)]; |
|
|
const newsItem = template |
|
|
.replace('${sector}', sectors[Math.floor(Math.random() * sectors.length)]) |
|
|
.replace('${company}', companies[Math.floor(Math.random() * companies.length)]) |
|
|
.replace('${partner}', companies[Math.floor(Math.random() * companies.length)]) |
|
|
.replace('${coin}', coins[Math.floor(Math.random() * coins.length)]) |
|
|
.replace('${percent}', (Math.random() * 20).toFixed(2)) |
|
|
.replace('${time}', ['24h', '7d', '30d'][Math.floor(Math.random() * 3)]) |
|
|
.replace('${trend}', ['growth', 'decline', 'volatility', 'stability'][Math.floor(Math.random() * 4)]) |
|
|
.replace('${change}', ['increase', 'decrease'][Math.floor(Math.random() * 2)]); |
|
|
|
|
|
clone.items.push(newsItem); |
|
|
} |
|
|
|
|
|
return clone; |
|
|
} |
|
|
|
|
|
openWidgetModal(widget) { |
|
|
|
|
|
document.getElementById('modalTitle').textContent = widget.title; |
|
|
|
|
|
|
|
|
const modalBody = document.getElementById('modalBody'); |
|
|
modalBody.innerHTML = ''; |
|
|
|
|
|
if (widget.type === 'metric') { |
|
|
this.renderMetricModal(modalBody, widget); |
|
|
} else if (widget.type === 'uptime') { |
|
|
this.renderUptimeModal(modalBody, widget); |
|
|
} else if (widget.type === 'news') { |
|
|
this.renderNewsModal(modalBody, widget); |
|
|
} |
|
|
|
|
|
|
|
|
this.modal.show(); |
|
|
} |
|
|
|
|
|
renderMetricModal(container, widget) { |
|
|
|
|
|
let formattedValue; |
|
|
if (widget.format === 'currency') { |
|
|
formattedValue = new Intl.NumberFormat('en-US', { |
|
|
style: 'currency', |
|
|
currency: 'USD', |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
}).format(widget.currentValue); |
|
|
} else if (widget.format === 'percent') { |
|
|
formattedValue = new Intl.NumberFormat('en-US', { |
|
|
style: 'percent', |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
}).format(widget.currentValue); |
|
|
} else { |
|
|
formattedValue = new Intl.NumberFormat('en-US').format(widget.currentValue); |
|
|
} |
|
|
|
|
|
|
|
|
const expandedView = document.createElement('div'); |
|
|
|
|
|
|
|
|
const currentValue = document.createElement('div'); |
|
|
currentValue.className = 'text-center mb-4'; |
|
|
currentValue.innerHTML = ` |
|
|
<h2 class="display-4 fw-bold mb-2">${formattedValue}</h2> |
|
|
<span class="badge bg-${widget.color}">Current Value</span> |
|
|
`; |
|
|
|
|
|
|
|
|
const chartContainer = document.createElement('div'); |
|
|
chartContainer.className = 'chart-container mt-4'; |
|
|
chartContainer.style.height = '300px'; |
|
|
const canvas = document.createElement('canvas'); |
|
|
chartContainer.appendChild(canvas); |
|
|
|
|
|
|
|
|
expandedView.appendChild(currentValue); |
|
|
expandedView.appendChild(chartContainer); |
|
|
container.appendChild(expandedView); |
|
|
|
|
|
|
|
|
this.renderDetailedMetricChart(canvas, widget); |
|
|
} |
|
|
|
|
|
renderUptimeModal(container, widget) { |
|
|
|
|
|
const expandedView = document.createElement('div'); |
|
|
|
|
|
|
|
|
const status = document.createElement('div'); |
|
|
status.className = 'text-center mb-4'; |
|
|
status.innerHTML = ` |
|
|
<h2 class="display-4 fw-bold mb-2 text-${widget.currentValue === 'online' ? 'success' : 'danger'}"> |
|
|
${widget.currentValue.toUpperCase()} |
|
|
</h2> |
|
|
<span class="badge bg-${widget.color}">Current Status</span> |
|
|
`; |
|
|
|
|
|
|
|
|
const statsRow = document.createElement('div'); |
|
|
statsRow.className = 'd-flex justify-content-around mb-4'; |
|
|
|
|
|
|
|
|
const uptimeStat = document.createElement('div'); |
|
|
uptimeStat.className = 'text-center'; |
|
|
uptimeStat.innerHTML = ` |
|
|
<div class="fs-1 fw-bold">${(widget.uptime * 100).toFixed(2)}%</div> |
|
|
<div class="text-muted">Uptime</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const latencyStat = document.createElement('div'); |
|
|
latencyStat.className = 'text-center'; |
|
|
latencyStat.innerHTML = ` |
|
|
<div class="fs-1 fw-bold">${widget.latency}ms</div> |
|
|
<div class="text-muted">Latency</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const historyStat = document.createElement('div'); |
|
|
historyStat.className = 'text-center'; |
|
|
historyStat.innerHTML = ` |
|
|
<div class="fs-1 fw-bold">${widget.historyLabel}</div> |
|
|
<div class="text-muted">History</div> |
|
|
`; |
|
|
|
|
|
|
|
|
statsRow.appendChild(uptimeStat); |
|
|
statsRow.appendChild(latencyStat); |
|
|
statsRow.appendChild(historyStat); |
|
|
|
|
|
|
|
|
const chartContainer = document.createElement('div'); |
|
|
chartContainer.className = 'chart-container mt-4'; |
|
|
chartContainer.style.height = '300px'; |
|
|
const canvas = document.createElement('canvas'); |
|
|
chartContainer.appendChild(canvas); |
|
|
|
|
|
|
|
|
expandedView.appendChild(status); |
|
|
expandedView.appendChild(statsRow); |
|
|
expandedView.appendChild(chartContainer); |
|
|
container.appendChild(expandedView); |
|
|
|
|
|
|
|
|
this.renderDetailedUptimeChart(canvas, widget); |
|
|
} |
|
|
|
|
|
renderNewsModal(container, widget) { |
|
|
|
|
|
const expandedView = document.createElement('div'); |
|
|
|
|
|
|
|
|
const title = document.createElement('h3'); |
|
|
title.className = 'text-center mb-4'; |
|
|
title.textContent = `${widget.title} - Detailed View`; |
|
|
|
|
|
|
|
|
const newsList = document.createElement('div'); |
|
|
newsList.className = 'list-group'; |
|
|
|
|
|
widget.items.forEach((item, index) => { |
|
|
const newsItem = document.createElement('div'); |
|
|
newsItem.className = 'list-group-item list-group-item-action bg-secondary border-dark'; |
|
|
newsItem.innerHTML = ` |
|
|
<div class="d-flex w-100 justify-content-between"> |
|
|
<h5 class="mb-1">News Update #${index + 1}</h5> |
|
|
<small>${new Date().toLocaleTimeString()}</small> |
|
|
</div> |
|
|
<p class="mb-1">${item}</p> |
|
|
`; |
|
|
newsList.appendChild(newsItem); |
|
|
}); |
|
|
|
|
|
|
|
|
expandedView.appendChild(title); |
|
|
expandedView.appendChild(newsList); |
|
|
container.appendChild(expandedView); |
|
|
} |
|
|
|
|
|
renderDetailedMetricChart(canvas, widget) { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const labels = widget.history.map((_, i) => `Day ${i + 1}`); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: labels, |
|
|
datasets: [{ |
|
|
label: widget.title, |
|
|
data: widget.history, |
|
|
borderColor: `var(--bs-${widget.color})`, |
|
|
backgroundColor: `rgba(var(--bs-${widget.color}-rgb), 0.1)`, |
|
|
borderWidth: 2, |
|
|
fill: true, |
|
|
tension: 0.4 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top', |
|
|
}, |
|
|
tooltip: { |
|
|
enabled: true, |
|
|
mode: 'index', |
|
|
intersect: false |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
y: { |
|
|
beginAtZero: false |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
renderDetailedUptimeChart(canvas, widget) { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const labels = widget.history.map((_, i) => `Day ${i + 1}`); |
|
|
const data = widget.history.map(val => val === 1 ? 100 : 0); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: labels, |
|
|
datasets: [{ |
|
|
label: 'Uptime %', |
|
|
data: data, |
|
|
borderColor: `var(--bs-${widget.color})`, |
|
|
backgroundColor: `rgba(var(--bs-${widget.color}-rgb), 0.5)`, |
|
|
borderWidth: 1 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top', |
|
|
}, |
|
|
tooltip: { |
|
|
enabled: true, |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
return context.parsed.y === 100 ? 'Online' : 'Offline'; |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
y: { |
|
|
min: 0, |
|
|
max: 100, |
|
|
ticks: { |
|
|
callback: function(value) { |
|
|
return value + '%'; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
updateDateTime() { |
|
|
const now = new Date(); |
|
|
const options = { |
|
|
weekday: 'long', |
|
|
year: 'numeric', |
|
|
month: 'long', |
|
|
day: 'numeric', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
second: '2-digit' |
|
|
}; |
|
|
document.getElementById('datetime').textContent = now.toLocaleDateString('en-US', options); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new Dashboard(); |
|
|
}); |