Skip to content

Commit 6d2eadf

Browse files
author
Your Name
committed
v3.4.3: fix Alpine.raw() Proxy conflict causing infinite recursion crash in dashboard charts, plus dashboard loading/console-error fixes from this session
1 parent ebc865f commit 6d2eadf

4 files changed

Lines changed: 70 additions & 24 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.4.2
1+
3.4.3

web/static/js/app.js

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,13 @@ function dashboardPage() {
499499
_charts: {},
500500

501501
async init() {
502-
await Promise.all([this.loadStats(),this.loadServices(),this.loadSslAlerts()]);
503-
this.loading = false;
502+
try {
503+
await Promise.all([this.loadStats(),this.loadServices(),this.loadSslAlerts()]);
504+
} catch(e) {
505+
console.warn('[VortexPanel] Dashboard init had an error, showing what loaded so far:', e);
506+
} finally {
507+
this.loading = false;
508+
}
504509
this._waitForChartJs();
505510
setInterval(()=>this.loadStats(),5000);
506511
setInterval(()=>this.loadSslAlerts(),60000); // recheck every minute, not every 5s
@@ -512,12 +517,12 @@ function dashboardPage() {
512517
if (r.ok) this.sslAlerts = r.alerts || [];
513518
},
514519
async loadStats() {
515-
const r=await get('/api/dashboard/stats');
520+
const r = await get('/api/dashboard/stats').catch(()=>({ok:false}));
516521
if(r.ok) { this.stats=r; this._pushHistory(r); }
517522
},
518523
async loadServices() {
519-
const r=await get('/api/services');
520-
if(r.ok) this.services=r.services.slice(0,8);
524+
const r = await get('/api/services').catch(()=>({ok:false}));
525+
if(r.ok) this.services=(r.services||[]).slice(0,8);
521526
},
522527
go(page){ window.dispatchEvent(new CustomEvent('nav',{detail:{page}})); },
523528
ramPct() { const r=this.stats.ram; return (r && r.total) ? Math.round(r.used/r.total*100) : 0; },
@@ -583,9 +588,9 @@ function dashboardPage() {
583588
if (cpuRamEl && !this._charts.cpuram) {
584589
this._charts.cpuram = new Chart(cpuRamEl, {
585590
type: 'line',
586-
data: { labels: this._hist.labels, datasets: [
587-
{ label:'CPU %', data:this._hist.cpu, borderColor:c.cpu, backgroundColor:c.cpu+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
588-
{ label:'RAM %', data:this._hist.ram, borderColor:c.ram, backgroundColor:c.ram+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
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 },
589594
]},
590595
options: baseOpts(100, '%'),
591596
});
@@ -595,9 +600,9 @@ function dashboardPage() {
595600
if (netEl && !this._charts.net) {
596601
this._charts.net = new Chart(netEl, {
597602
type: 'line',
598-
data: { labels: this._hist.labels, datasets: [
599-
{ label:'↓ In', data:this._hist.netRx, borderColor:c.rx, backgroundColor:c.rx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
600-
{ label:'↑ Out', data:this._hist.netTx, borderColor:c.tx, backgroundColor:c.tx+'22', fill:true, tension:.35, pointRadius:0, borderWidth:2 },
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 },
601606
]},
602607
options: { ...baseOpts(undefined, ''),
603608
scales: { ...baseOpts(undefined,'').scales,
@@ -608,14 +613,21 @@ function dashboardPage() {
608613

609614
_updateCharts() {
610615
if (!this._charts.cpuram) { this._initCharts(); if(!this._charts.cpuram) return; }
611-
this._charts.cpuram.data.labels = this._hist.labels;
612-
this._charts.cpuram.data.datasets[0].data = this._hist.cpu;
613-
this._charts.cpuram.data.datasets[1].data = this._hist.ram;
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);
614626
this._charts.cpuram.update('none');
615627
if (this._charts.net) {
616-
this._charts.net.data.labels = this._hist.labels;
617-
this._charts.net.data.datasets[0].data = this._hist.netRx;
618-
this._charts.net.data.datasets[1].data = this._hist.netTx;
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);
619631
this._charts.net.update('none');
620632
}
621633
},
@@ -646,6 +658,14 @@ function websitesPage() {
646658
integrityStatus:{enabled:false,created:'',file_count:0},
647659
diskUsage:{loading:false,size_human:'',size_bytes:0,file_count:0,dir_count:0},
648660
integrityScanning:false, integrityResult:null, integrityLoading:false,
661+
// directory.path has a real backend endpoint (GET/PUT /api/websites/<domain>/directory)
662+
// and working load code that was missing this object to assign into.
663+
// antixss/accesslog have NO backend support anywhere in the codebase --
664+
// kept here only so the checkboxes don't throw console errors; they are
665+
// not wired to anything real yet and need actual backend work.
666+
directory: {path:'', antixss:false, accesslog:false},
667+
hotlink: {enabled:false, suffixes:'jpg,jpeg,gif,png,js,css', domains:'', response:'404'},
668+
limitRules: [], limitForm: {name:'', path:'/admin', user:'', pass:''},
649669
},
650670
drawerTabs:[
651671
{id:'domains',label:'🌐 Domain Manager'},
@@ -716,6 +736,9 @@ function websitesPage() {
716736
nodejsEnabled:false,nodejsForm:{app_path:s.path||'',startup:'index.js',port:'3000',runtime:'node'},
717737
maintEnabled:false,maintMessage:'We are performing scheduled maintenance.',
718738
sslEmail:'',sslKey:'',sslCert:'',sslOutput:'',sslInfo:'',
739+
directory: {path:'', antixss:false, accesslog:false},
740+
hotlink: {enabled:false, suffixes:'jpg,jpeg,gif,png,js,css', domains:'', response:'404'},
741+
limitRules: [], limitForm: {name:'', path:'/admin', user:'', pass:''},
719742
};
720743
this.refreshPhpVersions();
721744
this.loadDrawerTab();
@@ -754,7 +777,7 @@ function websitesPage() {
754777
else if(d.tab==='domains'){const r=await get('/api/websites/'+domain+'/domains');if(r.ok)d.domains=r.domains||[];}
755778
else if(d.tab==='directory'){const r=await get('/api/websites/'+domain+'/directory');if(r.ok)d.directory.path=r.path||d.site?.path||''; this.loadDiskUsage();}
756779
else if(d.tab==='rewrite'){const r=await get('/api/websites/'+domain+'/rewrite');if(r.ok)d.rewriteContent=r.content||'';const rt=await get('/api/websites/'+domain+'/rewrite/templates');if(rt.ok)d.rewriteTemplates=rt.templates||[];}
757-
else if(d.tab==='hotlink'){const r=await get('/api/websites/'+domain+'/hotlink');if(r.ok){d.hotlink.enabled=r.enabled||false;d.hotlink.suffixes=r.suffixes||'jpg,jpeg,gif,png,js,css';d.hotlink.domains=r.domains||'';d.hotlink.response=r.response||'404';}}
780+
else if(d.tab==='hotlink'){const r=await get('/api/websites/'+domain+'/hotlink');if(r.ok){d.hotlink.enabled=r.enabled||false;d.hotlink.suffixes=r.suffixes||'jpg,jpeg,gif,png,js,css';d.hotlink.domains=r.access_domain||'';d.hotlink.response=r.response||'404';}}
758781
else if(d.tab==='limit'){const r=await get('/api/websites/'+domain+'/limit-access');if(r.ok)d.limitRules=r.rules||[];}
759782
else if(d.tab==='logs'){const r=await get('/api/websites/'+domain+'/logs');if(r.ok){d.accessLog=r.access||'No access logs';d.errorLog=r.error||'No error logs';}}
760783
else if(d.tab==='integrity'){await this.loadIntegrityStatus();}
@@ -1920,6 +1943,7 @@ function filesPage() {
19201943
function aiAssistant() {
19211944
return {
19221945
open: false,
1946+
loggedIn: false,
19231947
configured: false,
19241948
modelName: 'NeonCodex',
19251949
messages: [], // [{role:'user'|'assistant', content}]
@@ -1946,6 +1970,10 @@ function aiAssistant() {
19461970
],
19471971

19481972
async init() {
1973+
const chk = await get('/api/auth/check').catch(()=>({ok:false}));
1974+
this.loggedIn = !!(chk.ok && chk.logged_in);
1975+
document.addEventListener('vortex-logged-in', () => { this.loggedIn = true; });
1976+
document.addEventListener('vortex-logged-out', () => { this.loggedIn = false; });
19491977
// Listen for sidebar button toggle
19501978
document.addEventListener('vortex-toggle-ai', () => {
19511979
this.open = !this.open;
@@ -2899,6 +2927,7 @@ function backupsPage() {
28992927
restoreModal: {show:false, name:'', type:'', target:'', customPath:''},
29002928
showUpload: false, uploadFile:null, uploadType:'website', uploadTarget:'',
29012929
uploading: false,
2930+
form: {website:{domain:''}, db:{name:''}},
29022931

29032932
async init() { await Promise.all([this.load(), this.loadInfo()]); document.addEventListener("vortex-logged-in", () => { this.init(); }); window.addEventListener("vp:page", (e) => { if(e.detail==="backups") this.load(); }); },
29042933

@@ -3532,9 +3561,12 @@ function securityPage() {
35323561
health: {config:{enabled:false, protocol:'http', check_path:'/health', interval_seconds:10,
35333562
timeout_seconds:3, unhealthy_threshold:3, healthy_threshold:2, servers:[]},
35343563
state:{}, service_active:false, log:''},
3564+
sites: [],
35353565

35363566
async init() {
35373567
if (window.__vpPendingSecurityTab) { this.tab = window.__vpPendingSecurityTab; window.__vpPendingSecurityTab = null; }
3568+
const sr = await get('/api/websites').catch(()=>({ok:false}));
3569+
if (sr.ok) this.sites = sr.sites || [];
35383570
await Promise.all([this.loadScore(), this.loadSSH()]); document.addEventListener("vortex-logged-in", () => { this.init(); }); window.addEventListener("vp:page", (e) => { if(e.detail==="security") { this.loadScore(); this.loadSSH(); } });
35393571
window.addEventListener('vp:module-changed', (e) => {
35403572
if (e.detail?.id === 'modsecurity') this.loadModsec();

web/static/js/vendor/chart.umd.min.js

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/templates/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
1313
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
1414
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css">
15-
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
15+
<script src="/static/js/vendor/chart.umd.min.js"></script>
1616
<!--TEMP
1717
<link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css">
1818
<script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
@@ -619,7 +619,7 @@
619619
</div>
620620
</div>
621621
<button class="btn btn-primary btn-sm" style="margin-top:16px"
622-
@click="post('/api/websites/'+drawer.site?.domain+'/hotlink',drawer.hotlink).then(r=>toast(r.ok?'Saved':'Failed',r.ok?'success':'error'))">Save</button>
622+
@click="post('/api/websites/'+drawer.site?.domain+'/hotlink',{enable:drawer.hotlink.enabled,suffixes:drawer.hotlink.suffixes,access_domain:drawer.hotlink.domains,response:drawer.hotlink.response}).then(r=>toast(r.ok?'Saved':'Failed',r.ok?'success':'error'))">Save</button>
623623
<div style="margin-top:14px;font-size:11px;color:var(--text-muted);line-height:1.8">
624624
• By default, resources are allowed to be accessed directly<br>
625625
• Multiple URL suffixes and domains should be separated by comma<br>
@@ -7492,9 +7492,9 @@
74927492
</div>
74937493
<div style="padding:18px">
74947494
<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px">Hashed with Argon2id (OWASP #1 recommended). Min 8 characters.</div>
7495-
<div class="form-group"><label class="form-label">Current Password</label><input class="form-input" type="password" x-model="pwForm.current"></div>
7496-
<div class="form-group"><label class="form-label">New Password</label><input class="form-input" type="password" x-model="pwForm.newpw"></div>
7497-
<div class="form-group"><label class="form-label">Confirm New Password</label><input class="form-input" type="password" x-model="pwForm.confirm"></div>
7495+
<div class="form-group"><label class="form-label">Current Password</label><input class="form-input" type="password" x-model="$store.vp.security.pwForm.current"></div>
7496+
<div class="form-group"><label class="form-label">New Password</label><input class="form-input" type="password" x-model="$store.vp.security.pwForm.newpw"></div>
7497+
<div class="form-group"><label class="form-label">Confirm New Password</label><input class="form-input" type="password" x-model="$store.vp.security.pwForm.confirm"></div>
74987498
<button class="btn btn-primary" @click="changePanelPassword()">Change Password</button>
74997499
</div>
75007500
</div>

0 commit comments

Comments
 (0)