@@ -98,7 +98,7 @@ export default {
9898 } ) ;
9999 }
100100
101- // Query 1: Active instances and their latest properties (Last 7 Days)
101+ // Query 1: Active instances and their latest properties (Last 30 Days)
102102 const sqlActive = `
103103 SELECT
104104 blob1 as instance_id,
@@ -107,22 +107,33 @@ export default {
107107 blob4 as arch,
108108 blob6 as country,
109109 blob5 as cpu,
110- max(double1) as cpu_cores,
111- max(double2) as ram,
112- max(double3) as cameras,
113- max(double4) as groups,
114- max(double5) as events,
115- max(double6) as gpu,
116- max(double7) as notifications
110+ timestamp,
111+ double1 as cpu_cores,
112+ double2 as ram,
113+ double3 as cameras,
114+ double4 as groups,
115+ double5 as events,
116+ double6 as gpu,
117+ double7 as notifications
117118 FROM vibenvr_telemetry_events
118- WHERE timestamp >= NOW() - INTERVAL '7' DAY
119- GROUP BY blob1, blob2, blob3, blob4, blob6, blob5
119+ WHERE timestamp >= NOW() - INTERVAL '30' DAY
120120 ` ;
121121
122122 const sqlTotal = `SELECT count(DISTINCT blob1) as total FROM vibenvr_telemetry_events` ;
123123
124+ const sqlActivity = `
125+ SELECT
126+ toStartOfDay(timestamp) as day,
127+ count() as pings,
128+ count(DISTINCT blob1) as uniques
129+ FROM vibenvr_telemetry_events
130+ WHERE timestamp >= NOW() - INTERVAL '30' DAY
131+ GROUP BY day
132+ ORDER BY day ASC
133+ ` ;
134+
124135 try {
125- const [ resActive , resTotal ] = await Promise . all ( [
136+ const [ resActive , resTotal , resActivity ] = await Promise . all ( [
126137 fetch ( `https://api.cloudflare.com/client/v4/accounts/${ env . ACCOUNT_ID } /analytics_engine/sql` , {
127138 method : 'POST' ,
128139 headers : { 'Authorization' : `Bearer ${ env . API_TOKEN } ` } ,
@@ -132,31 +143,35 @@ export default {
132143 method : 'POST' ,
133144 headers : { 'Authorization' : `Bearer ${ env . API_TOKEN } ` } ,
134145 body : sqlTotal
146+ } ) ,
147+ fetch ( `https://api.cloudflare.com/client/v4/accounts/${ env . ACCOUNT_ID } /analytics_engine/sql` , {
148+ method : 'POST' ,
149+ headers : { 'Authorization' : `Bearer ${ env . API_TOKEN } ` } ,
150+ body : sqlActivity
135151 } )
136152 ] ) ;
137153
138154 const activeStr = await resActive . text ( ) ;
139155 const totalStr = await resTotal . text ( ) ;
156+ const activityStr = await resActivity . text ( ) ;
140157
141158 if ( ! resActive . ok ) throw new Error ( "SQL API Error: " + activeStr ) ;
142159
143160 const activeData = JSON . parse ( activeStr ) . data || [ ] ;
144161 const totalData = JSON . parse ( totalStr ) . data || [ ] ;
162+ const activityData = JSON . parse ( activityStr ) . data || [ ] ;
145163
146164 let activeCount = activeData . length ;
147165 const totalCount = parseInt ( totalData [ 0 ] ?. total || "0" , 10 ) ;
148166
149- // Deduplicate instances: pick the best record for each ID
167+ // Deduplicate instances: pick the latest record for each ID based on timestamp
150168 const uniqueInstances = new Map ( ) ;
151169 for ( const row of activeData ) {
152170 const id = row . instance_id ;
171+ const ts = new Date ( row . timestamp ) . getTime ( ) ;
153172 const existing = uniqueInstances . get ( id ) ;
154173
155- // Logic to pick the record with more metadata (prefer identified CPU)
156- const hasCpu = row . cpu && row . cpu !== 'unknown' ;
157- const existingHasCpu = existing && existing . cpu && existing . cpu !== 'unknown' ;
158-
159- if ( ! existing || ( ! existingHasCpu && hasCpu ) ) {
174+ if ( ! existing || ts > new Date ( existing . timestamp ) . getTime ( ) ) {
160175 uniqueInstances . set ( id , row ) ;
161176 }
162177 }
@@ -167,6 +182,11 @@ export default {
167182 const stats = {
168183 active_installs : activeCount ,
169184 total_installs : Math . max ( activeCount , totalCount ) ,
185+ activity : activityData . map ( row => ( {
186+ date : row . day ,
187+ pings : Number ( row . pings ) || 0 ,
188+ uniques : Number ( row . uniques ) || 0
189+ } ) ) ,
170190 versions : [ ] ,
171191 countries : [ ] ,
172192 cpus : [ ] ,
@@ -568,7 +588,7 @@ export default {
568588<main class="main">
569589 <div class="page-title">
570590 <h1>Usage Dashboard</h1>
571- <p>Anonymous aggregate statistics from active VibeNVR installations · Last 7 days</p>
591+ <p>Anonymous aggregate statistics from active VibeNVR installations · Last 30 days</p>
572592 </div>
573593
574594 <!-- Error -->
@@ -591,7 +611,7 @@ export default {
591611 <div class="kpi-card">
592612 <div class="kpi-label"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> Active Installs</div>
593613 <div class="kpi-value" id="kpi-active">-</div>
594- <div class="kpi-sub">Last 7 days</div>
614+ <div class="kpi-sub">Last 30 days</div>
595615 </div>
596616 <div class="kpi-card">
597617 <div class="kpi-label"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg> Total Installs</div>
@@ -633,6 +653,14 @@ export default {
633653 </div>
634654 </div>
635655
656+ <!-- Row 0b: Activity Trend -->
657+ <div class="chart-row cols-1">
658+ <div class="card">
659+ <div class="chart-title"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> Activity Trend (Last 30 Days)</div>
660+ <div class="chart-wrap" style="height:300px"><canvas id="chart-activity"></canvas></div>
661+ </div>
662+ </div>
663+
636664 <!-- Row 1: Cameras + Groups distribution -->
637665 <div class="chart-row cols-2">
638666 <div class="card">
@@ -848,6 +876,57 @@ export default {
848876 });
849877 mkChart('chart-os', 'doughnut', prepData(lastData.os), pp);
850878 mkChart('chart-arch', 'doughnut', prepData(lastData.arch), pp);
879+
880+ // Activity Trend Chart
881+ const activityCtx = document.getElementById('chart-activity')?.getContext('2d');
882+ if (activityCtx) {
883+ if (charts['chart-activity']) charts['chart-activity'].destroy();
884+ const activityLabels = lastData.activity.map(d => {
885+ const date = new Date(d.date);
886+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
887+ });
888+ charts['chart-activity'] = new Chart(activityCtx, {
889+ type: 'line',
890+ data: {
891+ labels: activityLabels,
892+ datasets: [
893+ {
894+ label: 'Unique IDs',
895+ data: lastData.activity.map(d => d.uniques),
896+ borderColor: tok('primary'),
897+ backgroundColor: 'transparent',
898+ tension: 0.3,
899+ pointRadius: 4,
900+ borderWidth: 3
901+ },
902+ {
903+ label: 'Total Pings',
904+ data: lastData.activity.map(d => d.pings),
905+ borderColor: tok('accent'),
906+ backgroundColor: 'transparent',
907+ tension: 0.3,
908+ pointRadius: 0,
909+ borderWidth: 2,
910+ borderDash: [5, 5]
911+ }
912+ ]
913+ },
914+ options: {
915+ responsive: true, maintainAspectRatio: false,
916+ plugins: {
917+ legend: { position: 'top', labels: { color: tok('text') } },
918+ tooltip: {
919+ backgroundColor: tok('bg'), titleColor: tok('text'), bodyColor: tok('muted'),
920+ borderColor: tok('border'), borderWidth: 1, padding: 10, cornerRadius: 8
921+ }
922+ },
923+ scales: {
924+ x: { grid: { display: false }, ticks: { color: tok('muted'), maxRotation: 0 } },
925+ y: { grid: { color: tok('border') }, ticks: { color: tok('muted') }, beginAtZero: true }
926+ }
927+ }
928+ });
929+ }
851930 const distRaw = lastData.cameras_dist || [];
852931 mkChart('chart-cameras-dist', 'bar', { labels: distRaw.map(x=>x.name+' cam'), data: distRaw.map(x=>x.count) }, BAR_PALETTE());
853932 const gdistRaw = lastData.groups_dist || [];
0 commit comments