Skip to content

Commit 56ae3e6

Browse files
author
Your Name
committed
v3.4.3: correct the chart recursion fix — hist and charts now live in a closure outside Alpine reactivity entirely, not just Alpine.raw()-unwrapped on access
1 parent 6d2eadf commit 56ae3e6

3 files changed

Lines changed: 41 additions & 33 deletions

File tree

panel/routes/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from flask import Blueprint, render_template
2+
from panel.routes.update import get_current_version
23
main_bp = Blueprint('main', __name__)
34

45
@main_bp.route('/')
56
def index():
6-
return render_template('index.html')
7+
return render_template('index.html', panel_version=get_current_version())

web/static/js/app.js

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,16 @@ function rootApp() {
480480

481481
// --- DASHBOARD ------------------------------------------------------------------
482482
function dashboardPage() {
483+
// Chart history lives here, OUTSIDE the object Alpine wraps in reactivity below.
484+
// Alpine.raw() was tried first to unwrap the Proxy before handing arrays to
485+
// Chart.js, but a real browser console dump confirmed recursion crashes
486+
// persisted even with genuinely fresh, cache-busted code -- meaning that
487+
// approach wasn't fully solving it. Keeping this data in a closure variable
488+
// instead means it's plain JS from the start and can never become a Proxy,
489+
// so there's nothing left for Chart.js's own internal instrumentation to
490+
// conflict with.
491+
const hist = { labels:[], cpu:[], ram:[], netRx:[], netTx:[] };
492+
const charts = {};
483493
return {
484494
stats:{cpu:0,ram:0,disk:0,uptime:'',load:'',ramTotal:'',diskTotal:'',network:''},
485495
wsConflict:{conflict:false, active:[], message:''},
@@ -493,10 +503,7 @@ function dashboardPage() {
493503
],
494504
loading: true,
495505
sslAlerts: [],
496-
// Rolling client-side history for charts (last 30 polls ≈ 2.5 min at 5s interval)
497-
_hist: { labels:[], cpu:[], ram:[], netRx:[], netTx:[] },
498506
_prevNet: null,
499-
_charts: {},
500507

501508
async init() {
502509
try {
@@ -531,7 +538,7 @@ function dashboardPage() {
531538
_pushHistory(r) {
532539
const now = new Date();
533540
const label = now.getHours().toString().padStart(2,'0')+':'+now.getMinutes().toString().padStart(2,'0')+':'+now.getSeconds().toString().padStart(2,'0');
534-
const h = this._hist;
541+
const h = hist;
535542
h.labels.push(label);
536543
h.cpu.push(r.cpu || 0);
537544
h.ram.push(this.ramPct());
@@ -585,24 +592,24 @@ function dashboardPage() {
585592
});
586593

587594
const cpuRamEl = document.getElementById('vp-chart-cpuram');
588-
if (cpuRamEl && !this._charts.cpuram) {
589-
this._charts.cpuram = new Chart(cpuRamEl, {
595+
if (cpuRamEl && !charts.cpuram) {
596+
charts.cpuram = new Chart(cpuRamEl, {
590597
type: 'line',
591-
data: { labels: Alpine.raw(this._hist.labels), datasets: [
592-
{ label:'CPU %', data:Alpine.raw(this._hist.cpu), borderColor:c.cpu, backgroundColor:c.cpu+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
593-
{ label:'RAM %', data:Alpine.raw(this._hist.ram), borderColor:c.ram, backgroundColor:c.ram+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
598+
data: { labels: hist.labels, datasets: [
599+
{ label:'CPU %', data:hist.cpu, borderColor:c.cpu, backgroundColor:c.cpu+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
600+
{ label:'RAM %', data:hist.ram, borderColor:c.ram, backgroundColor:c.ram+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
594601
]},
595602
options: baseOpts(100, '%'),
596603
});
597604
}
598605

599606
const netEl = document.getElementById('vp-chart-net');
600-
if (netEl && !this._charts.net) {
601-
this._charts.net = new Chart(netEl, {
607+
if (netEl && !charts.net) {
608+
charts.net = new Chart(netEl, {
602609
type: 'line',
603-
data: { labels: Alpine.raw(this._hist.labels), datasets: [
604-
{ label:'↓ In', data:Alpine.raw(this._hist.netRx), borderColor:c.rx, backgroundColor:c.rx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
605-
{ label:'↑ Out', data:Alpine.raw(this._hist.netTx), borderColor:c.tx, backgroundColor:c.tx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
610+
data: { labels: hist.labels, datasets: [
611+
{ label:'↓ In', data:hist.netRx, borderColor:c.rx, backgroundColor:c.rx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
612+
{ label:'↑ Out', data:hist.netTx, borderColor:c.tx, backgroundColor:c.tx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
606613
]},
607614
options: { ...baseOpts(undefined, ''),
608615
scales: { ...baseOpts(undefined,'').scales,
@@ -612,23 +619,23 @@ function dashboardPage() {
612619
},
613620

614621
_updateCharts() {
615-
if (!this._charts.cpuram) { this._initCharts(); if(!this._charts.cpuram) return; }
616-
// Alpine.raw() unwraps the reactive Proxy before handing arrays to
617-
// Chart.js -- confirmed via a real browser console dump that passing
618-
// the raw reactive Proxy directly caused an infinite recursion crash
619-
// (Chart.js's own internal property reads kept re-triggering Alpine's
620-
// Proxy traps, which re-triggered Chart.js, forever). This is Alpine's
621-
// documented pattern for integrating with libraries that do their own
622-
// internal instrumentation on the data they're given.
623-
this._charts.cpuram.data.labels = Alpine.raw(this._hist.labels);
624-
this._charts.cpuram.data.datasets[0].data = Alpine.raw(this._hist.cpu);
625-
this._charts.cpuram.data.datasets[1].data = Alpine.raw(this._hist.ram);
626-
this._charts.cpuram.update('none');
627-
if (this._charts.net) {
628-
this._charts.net.data.labels = Alpine.raw(this._hist.labels);
629-
this._charts.net.data.datasets[0].data = Alpine.raw(this._hist.netRx);
630-
this._charts.net.data.datasets[1].data = Alpine.raw(this._hist.netTx);
631-
this._charts.net.update('none');
622+
if (!charts.cpuram) { this._initCharts(); if(!charts.cpuram) return; }
623+
// hist is a plain closure variable (see top of dashboardPage), never
624+
// touched by Alpine's reactivity -- confirmed via a real browser
625+
// console dump that an earlier Alpine.raw()-unwrapping approach still
626+
// caused an infinite recursion crash even with genuinely fresh code.
627+
// Keeping this data outside Alpine's reactive object entirely means
628+
// there's nothing for Chart.js's own internal instrumentation to
629+
// conflict with.
630+
charts.cpuram.data.labels = hist.labels;
631+
charts.cpuram.data.datasets[0].data = hist.cpu;
632+
charts.cpuram.data.datasets[1].data = hist.ram;
633+
charts.cpuram.update('none');
634+
if (charts.net) {
635+
charts.net.data.labels = hist.labels;
636+
charts.net.data.datasets[0].data = hist.netRx;
637+
charts.net.data.datasets[1].data = hist.netTx;
638+
charts.net.update('none');
632639
}
633640
},
634641
};

web/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
-->
2121
<style>[x-cloak]{display:none!important}</style>
2222
<!-- app.js must load before Alpine so all function definitions exist -->
23-
<script src="/static/js/app.js"></script>
23+
<script src="/static/js/app.js?v={{ panel_version }}"></script>
2424
<script src="https://unpkg.com/alpinejs@3.14.9/dist/cdn.min.js" defer></script>
2525
</head>
2626
<body>

0 commit comments

Comments
 (0)