personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: align stats dashboard with schema v2 field names and structure

Update dashboard.js to read the backend's actual grouped data paths
(stats.totals.*, stats.tokens.*, stats.facets.counts_by_day,
stats.agents.counts_by_day) and per-day transcript_*/percept_* fields.
Add schema version check with warning, required-field validation,
freshness indicator, and DISPLAY_LABELS constant. Add freshness
element and CSS to workspace.html.

+65 -9
+57 -9
apps/stats/static/dashboard.js
··· 5 5 const Dashboard = (function() { 6 6 'use strict'; 7 7 8 + const EXPECTED_SCHEMA_VERSION = 2; 9 + const DISPLAY_LABELS = { transcript: 'Audio', percept: 'Screen' }; 10 + 8 11 // DOM element factory 9 12 function el(tag, attrs = {}, children = []) { 10 13 const elem = document.createElement(tag); ··· 45 48 return (value / 1e3).toFixed(1) + 'K'; 46 49 } 47 50 return String(Math.round(value)); 51 + } 52 + 53 + function fmtRelativeTime(isoString) { 54 + const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000); 55 + if (seconds < 60) return 'just now'; 56 + const minutes = Math.floor(seconds / 60); 57 + if (minutes < 60) return minutes + (minutes === 1 ? ' minute ago' : ' minutes ago'); 58 + const hours = Math.floor(minutes / 60); 59 + if (hours < 24) return hours + (hours === 1 ? ' hour ago' : ' hours ago'); 60 + const days = Math.floor(hours / 24); 61 + return days + (days === 1 ? ' day ago' : ' days ago'); 48 62 } 49 63 50 64 function fmtDay(raw) { ··· 547 561 ); 548 562 return; 549 563 } 564 + 565 + // Schema version check (non-blocking warning) 566 + if (stats.schema_version && stats.schema_version !== EXPECTED_SCHEMA_VERSION) { 567 + document.getElementById('notice').appendChild( 568 + el('div', {className: 'alert alert-warning'}, [ 569 + 'These stats were generated with an older format. Run ', 570 + el('code', {}, ['sol journal-stats']), 571 + ' to regenerate.' 572 + ]) 573 + ); 574 + } 575 + 576 + // Required-field validation (blocking — stops rendering if fields missing) 577 + const requiredFields = ['days', 'totals', 'heatmap', 'tokens', 'agents', 'facets']; 578 + const missingFields = requiredFields.filter(f => !(f in stats)); 579 + if (missingFields.length > 0) { 580 + document.getElementById('notice').appendChild( 581 + el('div', {className: 'alert alert-error'}, [ 582 + 'Stats data is missing required fields: ' + missingFields.join(', ') + '. ', 583 + 'Run ', 584 + el('code', {}, ['sol journal-stats']), 585 + ' to regenerate.' 586 + ]) 587 + ); 588 + return; 589 + } 590 + 591 + // Freshness indicator 592 + const freshnessEl = document.getElementById('statsFreshness'); 593 + if (freshnessEl) { 594 + freshnessEl.textContent = stats.generated_at 595 + ? 'Stats generated ' + fmtRelativeTime(stats.generated_at) 596 + : ''; 597 + } 550 598 551 599 // Show main content 552 600 const main = document.getElementById('mainContent'); ··· 567 615 const days = Object.keys(stats.days).sort(); 568 616 const totals = stats.totals || {}; 569 617 const totalDays = days.length; 570 - const totalAudioHours = Math.round((stats.total_audio_duration || 0) / 3600); 571 - const totalScreenHours = Math.round((stats.total_screen_duration || 0) / 3600); 618 + const totalAudioHours = Math.round((stats.totals.total_transcript_duration || 0) / 3600); 619 + const totalScreenHours = Math.round((stats.totals.total_percept_duration || 0) / 3600); 572 620 573 621 // Calculate total tokens across all models 574 - const tokenTotals = stats.token_totals_by_model || {}; 622 + const tokenTotals = stats.tokens.by_model || {}; 575 623 const totalTokens = Object.values(tokenTotals).reduce((sum, model) => { 576 624 return sum + (model.total_tokens || 0); 577 625 }, 0); ··· 589 637 const progressSection = document.getElementById('progressSection'); 590 638 progressSection.innerHTML = ''; // Clear existing content 591 639 progressSection.appendChild( 592 - progressCard('Audio Processing', totals.audio_sessions || 0, totals.pending_segments || 0) 640 + progressCard('Audio Processing', totals.transcript_sessions || 0, totals.pending_segments || 0) 593 641 ); 594 642 progressSection.appendChild( 595 643 progressCard('Agent Outputs', totals.outputs_processed || 0, totals.outputs_pending || 0) 596 644 ); 597 645 598 646 // Token usage setup 599 - const tokenUsage = stats.token_usage_by_day || {}; 647 + const tokenUsage = stats.tokens.by_day || {}; 600 648 const models = Object.keys(tokenTotals).sort(); 601 649 602 650 // Populate model selector ··· 633 681 const recent = days.slice(-30); 634 682 const hoursData = recent.map(day => { 635 683 const dayData = stats.days[day]; 636 - const audioHours = (dayData.audio_duration || 0) / 3600; 637 - const screenHours = (dayData.screen_duration || 0) / 3600; 684 + const audioHours = (dayData.transcript_duration || 0) / 3600; 685 + const screenHours = (dayData.percept_duration || 0) / 3600; 638 686 return { 639 687 day: day.slice(4, 6) + '/' + day.slice(6, 8), 640 688 audio: audioHours, ··· 654 702 // Render Facets stacked bar chart 655 703 buildStackedCategoryChart( 656 704 document.getElementById('facetsChart'), 657 - stats.facet_counts_by_day || {}, 705 + stats.facets.counts_by_day || {}, 658 706 { 659 707 emptyIcon: '🏷️', 660 708 emptyText: 'No facet data recorded', ··· 665 713 // Render Events stacked bar chart 666 714 buildStackedCategoryChart( 667 715 document.getElementById('eventsChart'), 668 - stats.agent_counts_by_day || {}, 716 + stats.agents.counts_by_day || {}, 669 717 Object.assign({}, data.generators || {}, { 670 718 emptyIcon: '⚡', 671 719 emptyText: 'No event data recorded',
+8
apps/stats/workspace.html
··· 56 56 color: #721c24; 57 57 } 58 58 59 + .stats-freshness { 60 + color: #888; 61 + font-size: 0.85em; 62 + margin: -0.5em 0 1em; 63 + } 64 + 59 65 /* Stats grid */ 60 66 .stats-grid { 61 67 display: grid; ··· 459 465 460 466 <div class="dashboard" data-stats-url="{{ url_for('app:stats.stats_data') }}"> 461 467 <h1>Journal Dashboard</h1> 468 + 469 + <p id="statsFreshness" class="stats-freshness"></p> 462 470 463 471 <div id="notice"></div> 464 472