Plot =require("@observablehq/plot@0.6")// Initialize statemutable isLoading =truemutable hasError =falsemutable errorMessage =""mutable timePeriod =30mutable lastRefresh =newDate()// Admin access checkisAdmin = {awaitnewPromise(resolve =>setTimeout(resolve,500));// Wait for auth// B8: ask the database (public.is_admin() over user_roles) rather than// window.Auth.isUserAdmin(), which is a mutable flag derived from a public list.if (typeofwindow.IoTClassSupabase?.isAdminServerSide==='function') {returnawaitwindow.IoTClassSupabase.isAdminServerSide(); }returnfalse;}
// Access denied viewaccessDenied =html`<div class="access-denied"> <h2>Access Denied</h2> <p>You must be logged in as an administrator to view this page.</p> <p>Please <a href="/hubs/dashboard.html">sign in</a> with an admin account.</p></div>`
Show code
analyticsData = {if (!isAdmin) returnnull; mutable isLoading =true; mutable hasError =false;try {awaitnewPromise(resolve =>setTimeout(resolve,100));// Get Analytics moduleconst Analytics =window.Analytics;if (!Analytics) {thrownewError("Analytics module not loaded"); }// Fetch all data in parallelconst [metrics, popular, viewsOverTime, lowEngagement, paths] =awaitPromise.all([ Analytics.getEngagementMetrics({ days: timePeriod }), Analytics.getPopularContent({ days: timePeriod,limit:20 }), Analytics.getViewsOverTime({ days: timePeriod }), Analytics.getLowEngagementContent({ days: timePeriod,limit:15 }), Analytics.getCommonPaths({ days: timePeriod,limit:10 }) ]); mutable isLoading =false; mutable lastRefresh =newDate();return { metrics, popular, viewsOverTime, lowEngagement, paths,period: timePeriod }; } catch (error) {console.error('[Analytics Dashboard] Error:', error); mutable hasError =true; mutable errorMessage = error.message; mutable isLoading =false;// Return mock data for demoreturngetMockData(timePeriod); }}// Mock data function for when Supabase is not availablefunctiongetMockData(days) {const baseViews =Math.floor(Math.random() *500) +500;const viewsPerDay = [];const today =newDate();for (let i = days; i >=0; i--) {const date =newDate(today); date.setDate(date.getDate() - i); viewsPerDay.push({date: date,dateStr: date.toISOString().split('T')[0],views:Math.floor(Math.random() *50) +20+ (i <7?15:0) }); }return {metrics: {totalViews: baseViews,uniqueUsers:Math.floor(baseViews *0.65),uniqueSessions:Math.floor(baseViews *0.8),avgTimeOnPage:245,avgTimeFormatted:"4:05",bounceRate:32.5,avgScrollDepth:68,completionRate:45.2,avgPagesPerSession:3.4 },popular: [ { title:"IoT Fundamentals",views:156,url:"/fundamentals/iot-overview.html",part:"Fundamentals" }, { title:"MQTT Protocol Deep Dive",views:134,url:"/app-protocols/mqtt-fundamentals.html",part:"Networking" }, { title:"LoRaWAN Architecture",views:121,url:"/lorawan/lorawan-overview.html",part:"Networking" }, { title:"Sensor Calibration",views:98,url:"/sensors/sensor-calibration-lab.html",part:"Sensing" }, { title:"Edge Computing",views:87,url:"/reference-architectures/edge.html",part:"Architectures" }, { title:"Security Best Practices",views:82,url:"/security-threats/iot-security-fundamentals.html",part:"Security" }, { title:"Data Analytics Intro",views:76,url:"/analytics-ml/analytics-overview.html",part:"Data" }, { title:"Smart Home Use Cases",views:71,url:"/applications/iot-use-cases-smart-home.html",part:"Applications" }, { title:"BLE Fundamentals",views:65,url:"/bluetooth-ble/bluetooth-fundamentals.html",part:"Networking" }, { title:"PID Control Systems",views:58,url:"/reference-architectures/pid-control-theory.html",part:"Sensing" } ],viewsOverTime: viewsPerDay,lowEngagement: [ { title:"Appendix A",views:12,bounceRate:78,avgScrollDepth:22,avgDuration:35,engagementScore:18 }, { title:"Legacy Protocols",views:8,bounceRate:72,avgScrollDepth:31,avgDuration:42,engagementScore:24 }, { title:"Historical Context",views:15,bounceRate:65,avgScrollDepth:38,avgDuration:55,engagementScore:32 } ],paths: [ { fromTitle:"Home",toTitle:"IoT Fundamentals",count:89 }, { fromTitle:"IoT Fundamentals",toTitle:"MQTT Protocol",count:67 }, { fromTitle:"Dashboard",toTitle:"My Progress",count:54 }, { fromTitle:"MQTT Protocol",toTitle:"LoRaWAN",count:43 }, { fromTitle:"Sensor Calibration",toTitle:"PID Control",count:38 } ],period: days };}
Bounce Rate: Percentage of visitors who leave within 30 seconds without scrolling
Scroll Depth: How far down the page users scroll on average
Completion Rate: Percentage of users who scroll past 75% of the content
Engagement Score: Combined metric (0-100) based on bounce rate, scroll depth, and time on page
Data Collection
Analytics data is collected in real-time as users browse the platform. All data is anonymized and used only to improve the learning experience. Page views are tracked with timestamp, duration, and scroll depth to help identify which content resonates most with learners.