overcode 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
overcode/web_templates.py CHANGED
@@ -561,3 +561,1096 @@ def get_dashboard_html() -> str:
561
561
  </script>
562
562
  </body>
563
563
  </html>"""
564
+
565
+
566
+ def get_analytics_html() -> str:
567
+ """Return the complete analytics dashboard HTML page."""
568
+ return """<!DOCTYPE html>
569
+ <html lang="en">
570
+ <head>
571
+ <meta charset="UTF-8">
572
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
573
+ <meta name="theme-color" content="#0f172a">
574
+ <title>Overcode Analytics</title>
575
+ <script src="/static/chart.min.js"></script>
576
+ <style>
577
+ :root {
578
+ --bg-primary: #0f172a;
579
+ --bg-secondary: #1e293b;
580
+ --bg-tertiary: #334155;
581
+ --bg-hover: #475569;
582
+ --text-primary: #f1f5f9;
583
+ --text-secondary: #94a3b8;
584
+ --text-dim: #64748b;
585
+ --green: #22c55e;
586
+ --yellow: #eab308;
587
+ --orange: #f97316;
588
+ --red: #ef4444;
589
+ --cyan: #06b6d4;
590
+ --purple: #a855f7;
591
+ --border: #334155;
592
+ }
593
+
594
+ * { box-sizing: border-box; margin: 0; padding: 0; }
595
+
596
+ body {
597
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
598
+ background: var(--bg-primary);
599
+ color: var(--text-primary);
600
+ min-height: 100vh;
601
+ font-size: 14px;
602
+ line-height: 1.5;
603
+ }
604
+
605
+ /* Navigation */
606
+ .nav {
607
+ background: var(--bg-secondary);
608
+ border-bottom: 1px solid var(--border);
609
+ padding: 12px 24px;
610
+ display: flex;
611
+ align-items: center;
612
+ gap: 24px;
613
+ position: sticky;
614
+ top: 0;
615
+ z-index: 100;
616
+ }
617
+
618
+ .nav-brand {
619
+ font-size: 18px;
620
+ font-weight: 700;
621
+ color: var(--orange);
622
+ text-decoration: none;
623
+ }
624
+
625
+ .nav-links {
626
+ display: flex;
627
+ gap: 4px;
628
+ }
629
+
630
+ .nav-link {
631
+ padding: 8px 16px;
632
+ border-radius: 6px;
633
+ color: var(--text-secondary);
634
+ text-decoration: none;
635
+ font-weight: 500;
636
+ transition: all 0.2s;
637
+ }
638
+
639
+ .nav-link:hover { background: var(--bg-tertiary); color: var(--text-primary); }
640
+ .nav-link.active { background: var(--bg-tertiary); color: var(--text-primary); }
641
+
642
+ .nav-right {
643
+ margin-left: auto;
644
+ display: flex;
645
+ align-items: center;
646
+ gap: 12px;
647
+ }
648
+
649
+ /* Time range selector */
650
+ .time-range {
651
+ background: var(--bg-tertiary);
652
+ border-radius: 8px;
653
+ padding: 12px 16px;
654
+ margin: 16px 24px;
655
+ display: flex;
656
+ flex-wrap: wrap;
657
+ align-items: center;
658
+ gap: 12px;
659
+ }
660
+
661
+ .time-range-label {
662
+ font-weight: 500;
663
+ color: var(--text-secondary);
664
+ }
665
+
666
+ .preset-btn {
667
+ padding: 6px 12px;
668
+ border-radius: 4px;
669
+ border: 1px solid var(--border);
670
+ background: var(--bg-secondary);
671
+ color: var(--text-secondary);
672
+ cursor: pointer;
673
+ font-size: 13px;
674
+ transition: all 0.2s;
675
+ }
676
+
677
+ .preset-btn:hover { border-color: var(--orange); color: var(--text-primary); }
678
+ .preset-btn.active { background: var(--orange); border-color: var(--orange); color: #000; font-weight: 500; }
679
+
680
+ .time-inputs {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 8px;
684
+ margin-left: auto;
685
+ }
686
+
687
+ .time-input {
688
+ padding: 6px 10px;
689
+ border-radius: 4px;
690
+ border: 1px solid var(--border);
691
+ background: var(--bg-secondary);
692
+ color: var(--text-primary);
693
+ font-size: 13px;
694
+ }
695
+
696
+ .time-input:focus { outline: none; border-color: var(--orange); }
697
+
698
+ .refresh-btn {
699
+ padding: 6px 12px;
700
+ border-radius: 4px;
701
+ border: none;
702
+ background: var(--green);
703
+ color: #000;
704
+ cursor: pointer;
705
+ font-weight: 500;
706
+ display: flex;
707
+ align-items: center;
708
+ gap: 4px;
709
+ }
710
+
711
+ .refresh-btn:hover { opacity: 0.9; }
712
+
713
+ /* Main content */
714
+ .main { padding: 0 24px 24px; max-width: 1400px; margin: 0 auto; }
715
+
716
+ /* Page containers */
717
+ .page { display: none; }
718
+ .page.active { display: block; }
719
+
720
+ /* Summary cards */
721
+ .summary-grid {
722
+ display: grid;
723
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
724
+ gap: 16px;
725
+ margin-bottom: 24px;
726
+ }
727
+
728
+ .summary-card {
729
+ background: var(--bg-secondary);
730
+ border-radius: 8px;
731
+ padding: 20px;
732
+ border-left: 4px solid var(--border);
733
+ }
734
+
735
+ .summary-card.green { border-left-color: var(--green); }
736
+ .summary-card.orange { border-left-color: var(--orange); }
737
+ .summary-card.yellow { border-left-color: var(--yellow); }
738
+ .summary-card.cyan { border-left-color: var(--cyan); }
739
+
740
+ .summary-value {
741
+ font-size: 32px;
742
+ font-weight: 700;
743
+ color: var(--text-primary);
744
+ margin-bottom: 4px;
745
+ }
746
+
747
+ .summary-label {
748
+ font-size: 13px;
749
+ color: var(--text-secondary);
750
+ text-transform: uppercase;
751
+ letter-spacing: 0.5px;
752
+ }
753
+
754
+ /* Chart containers */
755
+ .chart-container {
756
+ background: var(--bg-secondary);
757
+ border-radius: 8px;
758
+ padding: 20px;
759
+ margin-bottom: 24px;
760
+ }
761
+
762
+ .chart-title {
763
+ font-size: 16px;
764
+ font-weight: 600;
765
+ margin-bottom: 16px;
766
+ color: var(--text-primary);
767
+ }
768
+
769
+ .chart-wrapper { position: relative; height: 300px; }
770
+
771
+ /* Sessions table */
772
+ .sessions-table {
773
+ width: 100%;
774
+ border-collapse: collapse;
775
+ background: var(--bg-secondary);
776
+ border-radius: 8px;
777
+ overflow: hidden;
778
+ }
779
+
780
+ .sessions-table th {
781
+ text-align: left;
782
+ padding: 12px 16px;
783
+ background: var(--bg-tertiary);
784
+ font-weight: 600;
785
+ color: var(--text-secondary);
786
+ font-size: 12px;
787
+ text-transform: uppercase;
788
+ letter-spacing: 0.5px;
789
+ cursor: pointer;
790
+ user-select: none;
791
+ }
792
+
793
+ .sessions-table th:hover { color: var(--text-primary); }
794
+ .sessions-table th.sorted { color: var(--orange); }
795
+ .sessions-table th .sort-icon { margin-left: 4px; }
796
+
797
+ .sessions-table td {
798
+ padding: 12px 16px;
799
+ border-top: 1px solid var(--border);
800
+ color: var(--text-primary);
801
+ }
802
+
803
+ .sessions-table tr:hover { background: var(--bg-tertiary); }
804
+
805
+ .session-name {
806
+ font-weight: 600;
807
+ display: flex;
808
+ align-items: center;
809
+ gap: 8px;
810
+ }
811
+
812
+ .session-badge {
813
+ font-size: 10px;
814
+ padding: 2px 6px;
815
+ border-radius: 4px;
816
+ background: var(--bg-tertiary);
817
+ color: var(--text-dim);
818
+ }
819
+
820
+ .session-badge.active { background: var(--green); color: #000; }
821
+
822
+ .expandable-row { cursor: pointer; }
823
+ .expanded-content {
824
+ display: none;
825
+ background: var(--bg-tertiary);
826
+ padding: 16px;
827
+ }
828
+
829
+ .expanded-content.show { display: table-row; }
830
+
831
+ /* Timeline visualization */
832
+ .timeline-container {
833
+ background: var(--bg-secondary);
834
+ border-radius: 8px;
835
+ padding: 20px;
836
+ }
837
+
838
+ .timeline-header {
839
+ display: flex;
840
+ justify-content: space-between;
841
+ margin-bottom: 16px;
842
+ }
843
+
844
+ .timeline-controls {
845
+ display: flex;
846
+ gap: 8px;
847
+ }
848
+
849
+ .timeline-btn {
850
+ padding: 4px 8px;
851
+ border-radius: 4px;
852
+ border: 1px solid var(--border);
853
+ background: transparent;
854
+ color: var(--text-secondary);
855
+ cursor: pointer;
856
+ font-size: 12px;
857
+ }
858
+
859
+ .timeline-btn:hover { border-color: var(--orange); color: var(--text-primary); }
860
+
861
+ .timeline-row {
862
+ display: flex;
863
+ align-items: center;
864
+ margin-bottom: 8px;
865
+ }
866
+
867
+ .timeline-label {
868
+ width: 120px;
869
+ font-size: 13px;
870
+ color: var(--text-secondary);
871
+ overflow: hidden;
872
+ text-overflow: ellipsis;
873
+ white-space: nowrap;
874
+ }
875
+
876
+ .timeline-bar {
877
+ flex: 1;
878
+ height: 24px;
879
+ background: var(--bg-tertiary);
880
+ border-radius: 4px;
881
+ overflow: hidden;
882
+ position: relative;
883
+ }
884
+
885
+ .timeline-segment {
886
+ position: absolute;
887
+ top: 0;
888
+ height: 100%;
889
+ }
890
+
891
+ .timeline-legend {
892
+ display: flex;
893
+ gap: 16px;
894
+ margin-top: 16px;
895
+ font-size: 12px;
896
+ color: var(--text-dim);
897
+ }
898
+
899
+ .legend-item {
900
+ display: flex;
901
+ align-items: center;
902
+ gap: 6px;
903
+ }
904
+
905
+ .legend-color {
906
+ width: 12px;
907
+ height: 12px;
908
+ border-radius: 2px;
909
+ }
910
+
911
+ /* Metrics grid */
912
+ .metrics-grid {
913
+ display: grid;
914
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
915
+ gap: 24px;
916
+ }
917
+
918
+ .metric-card {
919
+ background: var(--bg-secondary);
920
+ border-radius: 8px;
921
+ padding: 20px;
922
+ }
923
+
924
+ .metric-title {
925
+ font-size: 14px;
926
+ font-weight: 600;
927
+ color: var(--text-secondary);
928
+ margin-bottom: 12px;
929
+ }
930
+
931
+ .metric-value {
932
+ font-size: 28px;
933
+ font-weight: 700;
934
+ color: var(--text-primary);
935
+ }
936
+
937
+ .metric-sub {
938
+ font-size: 12px;
939
+ color: var(--text-dim);
940
+ margin-top: 4px;
941
+ }
942
+
943
+ .work-times-table {
944
+ width: 100%;
945
+ margin-top: 12px;
946
+ }
947
+
948
+ .work-times-table td {
949
+ padding: 4px 8px;
950
+ font-size: 13px;
951
+ }
952
+
953
+ .work-times-table td:first-child {
954
+ color: var(--text-secondary);
955
+ }
956
+
957
+ .work-times-table td:last-child {
958
+ text-align: right;
959
+ font-weight: 500;
960
+ }
961
+
962
+ /* Loading and empty states */
963
+ .loading, .empty {
964
+ text-align: center;
965
+ padding: 48px;
966
+ color: var(--text-dim);
967
+ }
968
+
969
+ .spinner {
970
+ width: 32px;
971
+ height: 32px;
972
+ border: 3px solid var(--border);
973
+ border-top-color: var(--orange);
974
+ border-radius: 50%;
975
+ animation: spin 1s linear infinite;
976
+ margin: 0 auto 16px;
977
+ }
978
+
979
+ @keyframes spin { to { transform: rotate(360deg); } }
980
+
981
+ /* Responsive */
982
+ @media (max-width: 768px) {
983
+ .nav { padding: 12px 16px; flex-wrap: wrap; }
984
+ .nav-right { width: 100%; justify-content: flex-end; margin-top: 8px; }
985
+ .time-range { margin: 12px 16px; flex-direction: column; align-items: stretch; }
986
+ .time-inputs { margin-left: 0; margin-top: 8px; flex-wrap: wrap; }
987
+ .main { padding: 0 16px 16px; }
988
+ .sessions-table { font-size: 12px; }
989
+ .sessions-table th, .sessions-table td { padding: 8px; }
990
+ }
991
+ </style>
992
+ </head>
993
+ <body>
994
+ <nav class="nav">
995
+ <a href="#" class="nav-brand">Overcode Analytics</a>
996
+ <div class="nav-links">
997
+ <a href="#dashboard" class="nav-link active" data-page="dashboard">Dashboard</a>
998
+ <a href="#sessions" class="nav-link" data-page="sessions">Sessions</a>
999
+ <a href="#timeline" class="nav-link" data-page="timeline">Timeline</a>
1000
+ <a href="#efficiency" class="nav-link" data-page="efficiency">Efficiency</a>
1001
+ </div>
1002
+ <div class="nav-right">
1003
+ <span id="last-update" style="font-size: 12px; color: var(--text-dim);">Loading...</span>
1004
+ </div>
1005
+ </nav>
1006
+
1007
+ <div class="time-range">
1008
+ <span class="time-range-label">Time Range:</span>
1009
+ <div id="presets-container"></div>
1010
+ <div class="time-inputs">
1011
+ <input type="date" class="time-input" id="start-date">
1012
+ <input type="time" class="time-input" id="start-time" value="00:00">
1013
+ <span style="color: var(--text-dim);">to</span>
1014
+ <input type="date" class="time-input" id="end-date">
1015
+ <input type="time" class="time-input" id="end-time" value="23:59">
1016
+ <button class="preset-btn" id="apply-range">Apply</button>
1017
+ <button class="refresh-btn" id="refresh-btn">Refresh</button>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <main class="main">
1022
+ <!-- Dashboard Page -->
1023
+ <div id="page-dashboard" class="page active">
1024
+ <div id="dashboard-summary" class="summary-grid"></div>
1025
+ <div class="chart-container">
1026
+ <h3 class="chart-title">Daily Activity</h3>
1027
+ <div class="chart-wrapper">
1028
+ <canvas id="daily-chart"></canvas>
1029
+ </div>
1030
+ </div>
1031
+ <div class="chart-container">
1032
+ <h3 class="chart-title">Recent Sessions</h3>
1033
+ <div id="recent-sessions"></div>
1034
+ </div>
1035
+ </div>
1036
+
1037
+ <!-- Sessions Page -->
1038
+ <div id="page-sessions" class="page">
1039
+ <div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center;">
1040
+ <h2 style="font-size: 20px;">Session History</h2>
1041
+ <label style="display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 13px;">
1042
+ <input type="checkbox" id="show-all-sessions"> Show all (ignore time filter)
1043
+ </label>
1044
+ </div>
1045
+ <div id="sessions-table-container"></div>
1046
+ </div>
1047
+
1048
+ <!-- Timeline Page -->
1049
+ <div id="page-timeline" class="page">
1050
+ <div class="timeline-container">
1051
+ <div class="timeline-header">
1052
+ <h2 style="font-size: 20px;">Agent Timeline</h2>
1053
+ <div class="timeline-controls">
1054
+ <button class="timeline-btn" id="zoom-in">Zoom In</button>
1055
+ <button class="timeline-btn" id="zoom-out">Zoom Out</button>
1056
+ </div>
1057
+ </div>
1058
+ <div id="timeline-content"></div>
1059
+ <h3 class="chart-title" style="margin-top: 24px;">User Presence</h3>
1060
+ <div id="presence-timeline"></div>
1061
+ <div class="timeline-legend">
1062
+ <div class="legend-item"><div class="legend-color" style="background: var(--green)"></div> Running</div>
1063
+ <div class="legend-item"><div class="legend-color" style="background: var(--yellow)"></div> Idle/Waiting</div>
1064
+ <div class="legend-item"><div class="legend-color" style="background: var(--orange)"></div> Needs Input</div>
1065
+ <div class="legend-item"><div class="legend-color" style="background: var(--red)"></div> Error/Blocked</div>
1066
+ <div class="legend-item"><div class="legend-color" style="background: var(--text-dim)"></div> Terminated</div>
1067
+ </div>
1068
+ </div>
1069
+ </div>
1070
+
1071
+ <!-- Efficiency Page -->
1072
+ <div id="page-efficiency" class="page">
1073
+ <div id="efficiency-summary" class="summary-grid"></div>
1074
+ <div class="metrics-grid">
1075
+ <div class="metric-card">
1076
+ <div class="metric-title">Presence Efficiency</div>
1077
+ <div id="presence-efficiency-metrics"></div>
1078
+ </div>
1079
+ <div class="metric-card">
1080
+ <div class="metric-title">Cost Efficiency</div>
1081
+ <div id="cost-metrics"></div>
1082
+ </div>
1083
+ <div class="metric-card">
1084
+ <div class="metric-title">Work Cycle Times</div>
1085
+ <div id="work-time-metrics"></div>
1086
+ </div>
1087
+ <div class="metric-card">
1088
+ <div class="metric-title">Interaction Breakdown</div>
1089
+ <div id="interaction-metrics"></div>
1090
+ </div>
1091
+ </div>
1092
+ <div class="chart-container" style="margin-top: 24px;">
1093
+ <h3 class="chart-title">Efficiency Trend</h3>
1094
+ <div class="chart-wrapper">
1095
+ <canvas id="efficiency-chart"></canvas>
1096
+ </div>
1097
+ </div>
1098
+ </div>
1099
+ </main>
1100
+
1101
+ <script>
1102
+ // State
1103
+ let state = {
1104
+ currentPage: 'dashboard',
1105
+ presets: [],
1106
+ activePreset: null,
1107
+ startDate: null,
1108
+ endDate: null,
1109
+ data: {
1110
+ sessions: null,
1111
+ timeline: null,
1112
+ stats: null,
1113
+ daily: null,
1114
+ },
1115
+ charts: {
1116
+ daily: null,
1117
+ efficiency: null,
1118
+ },
1119
+ sortColumn: 'start_time',
1120
+ sortAsc: false,
1121
+ };
1122
+
1123
+ // Initialize
1124
+ async function init() {
1125
+ await loadPresets();
1126
+ setupNavigation();
1127
+ setupTimeRangeControls();
1128
+ loadFromHash();
1129
+ await refreshData();
1130
+ }
1131
+
1132
+ // Load presets from API
1133
+ async function loadPresets() {
1134
+ try {
1135
+ const resp = await fetch('/api/analytics/presets');
1136
+ state.presets = await resp.json();
1137
+ } catch (e) {
1138
+ state.presets = [
1139
+ { name: 'Morning', start: '09:00', end: '12:00' },
1140
+ { name: 'Full Day', start: '09:00', end: '17:00' },
1141
+ { name: 'All Time', start: null, end: null },
1142
+ ];
1143
+ }
1144
+ renderPresets();
1145
+ }
1146
+
1147
+ function renderPresets() {
1148
+ const container = document.getElementById('presets-container');
1149
+ container.innerHTML = state.presets.map(p => `
1150
+ <button class="preset-btn ${p.name === state.activePreset ? 'active' : ''}"
1151
+ data-preset="${p.name}" data-start="${p.start || ''}" data-end="${p.end || ''}">
1152
+ ${p.name}
1153
+ </button>
1154
+ `).join('');
1155
+
1156
+ container.querySelectorAll('.preset-btn').forEach(btn => {
1157
+ btn.addEventListener('click', () => applyPreset(btn.dataset.preset, btn.dataset.start, btn.dataset.end));
1158
+ });
1159
+ }
1160
+
1161
+ function applyPreset(name, startTime, endTime) {
1162
+ state.activePreset = name;
1163
+ const today = new Date().toISOString().split('T')[0];
1164
+
1165
+ if (!startTime && !endTime) {
1166
+ // All time
1167
+ state.startDate = null;
1168
+ state.endDate = null;
1169
+ } else {
1170
+ state.startDate = `${today}T${startTime}:00`;
1171
+ state.endDate = `${today}T${endTime}:00`;
1172
+ }
1173
+
1174
+ updateHash();
1175
+ renderPresets();
1176
+ refreshData();
1177
+ }
1178
+
1179
+ function setupTimeRangeControls() {
1180
+ const today = new Date().toISOString().split('T')[0];
1181
+ document.getElementById('start-date').value = today;
1182
+ document.getElementById('end-date').value = today;
1183
+
1184
+ document.getElementById('apply-range').addEventListener('click', () => {
1185
+ const sd = document.getElementById('start-date').value;
1186
+ const st = document.getElementById('start-time').value;
1187
+ const ed = document.getElementById('end-date').value;
1188
+ const et = document.getElementById('end-time').value;
1189
+
1190
+ state.startDate = `${sd}T${st}:00`;
1191
+ state.endDate = `${ed}T${et}:00`;
1192
+ state.activePreset = null;
1193
+ updateHash();
1194
+ renderPresets();
1195
+ refreshData();
1196
+ });
1197
+
1198
+ document.getElementById('refresh-btn').addEventListener('click', refreshData);
1199
+ }
1200
+
1201
+ function setupNavigation() {
1202
+ document.querySelectorAll('.nav-link').forEach(link => {
1203
+ link.addEventListener('click', (e) => {
1204
+ e.preventDefault();
1205
+ navigateTo(link.dataset.page);
1206
+ });
1207
+ });
1208
+ }
1209
+
1210
+ function navigateTo(page) {
1211
+ state.currentPage = page;
1212
+ document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
1213
+ document.querySelector(`[data-page="${page}"]`).classList.add('active');
1214
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
1215
+ document.getElementById(`page-${page}`).classList.add('active');
1216
+ updateHash();
1217
+ }
1218
+
1219
+ function updateHash() {
1220
+ const params = new URLSearchParams();
1221
+ params.set('page', state.currentPage);
1222
+ if (state.startDate) params.set('start', state.startDate);
1223
+ if (state.endDate) params.set('end', state.endDate);
1224
+ if (state.activePreset) params.set('preset', state.activePreset);
1225
+ window.location.hash = params.toString();
1226
+ }
1227
+
1228
+ function loadFromHash() {
1229
+ const hash = window.location.hash.slice(1);
1230
+ if (!hash) return;
1231
+
1232
+ const params = new URLSearchParams(hash);
1233
+ if (params.get('page')) state.currentPage = params.get('page');
1234
+ if (params.get('start')) state.startDate = params.get('start');
1235
+ if (params.get('end')) state.endDate = params.get('end');
1236
+ if (params.get('preset')) state.activePreset = params.get('preset');
1237
+
1238
+ navigateTo(state.currentPage);
1239
+ renderPresets();
1240
+ }
1241
+
1242
+ // Data fetching
1243
+ async function refreshData() {
1244
+ document.getElementById('last-update').textContent = 'Loading...';
1245
+
1246
+ const params = new URLSearchParams();
1247
+ if (state.startDate) params.set('start', state.startDate);
1248
+ if (state.endDate) params.set('end', state.endDate);
1249
+ const qs = params.toString() ? '?' + params.toString() : '';
1250
+
1251
+ try {
1252
+ const [sessions, timeline, stats, daily] = await Promise.all([
1253
+ fetch('/api/analytics/sessions' + qs).then(r => r.json()),
1254
+ fetch('/api/analytics/timeline' + qs).then(r => r.json()),
1255
+ fetch('/api/analytics/stats' + qs).then(r => r.json()),
1256
+ fetch('/api/analytics/daily' + qs).then(r => r.json()),
1257
+ ]);
1258
+
1259
+ state.data = { sessions, timeline, stats, daily };
1260
+ renderAll();
1261
+ document.getElementById('last-update').textContent = 'Updated ' + new Date().toLocaleTimeString();
1262
+ } catch (e) {
1263
+ console.error('Failed to fetch data:', e);
1264
+ document.getElementById('last-update').textContent = 'Error loading data';
1265
+ }
1266
+ }
1267
+
1268
+ // Rendering
1269
+ function renderAll() {
1270
+ renderDashboard();
1271
+ renderSessions();
1272
+ renderTimeline();
1273
+ renderEfficiency();
1274
+ }
1275
+
1276
+ function renderDashboard() {
1277
+ const s = state.data.sessions?.summary || {};
1278
+ document.getElementById('dashboard-summary').innerHTML = `
1279
+ <div class="summary-card green">
1280
+ <div class="summary-value">${s.session_count || 0}</div>
1281
+ <div class="summary-label">Sessions</div>
1282
+ </div>
1283
+ <div class="summary-card orange">
1284
+ <div class="summary-value">${formatTokens(s.total_tokens || 0)}</div>
1285
+ <div class="summary-label">Total Tokens</div>
1286
+ </div>
1287
+ <div class="summary-card yellow">
1288
+ <div class="summary-value">$${(s.total_cost_usd || 0).toFixed(2)}</div>
1289
+ <div class="summary-label">Total Cost</div>
1290
+ </div>
1291
+ <div class="summary-card cyan">
1292
+ <div class="summary-value">${s.avg_green_percent || 0}%</div>
1293
+ <div class="summary-label">Avg Green Time</div>
1294
+ </div>
1295
+ `;
1296
+
1297
+ renderDailyChart();
1298
+ renderRecentSessions();
1299
+ }
1300
+
1301
+ function renderDailyChart() {
1302
+ const daily = state.data.daily;
1303
+ if (!daily?.days?.length) return;
1304
+
1305
+ const ctx = document.getElementById('daily-chart').getContext('2d');
1306
+ if (state.charts.daily) state.charts.daily.destroy();
1307
+
1308
+ state.charts.daily = new Chart(ctx, {
1309
+ type: 'bar',
1310
+ data: {
1311
+ labels: daily.labels,
1312
+ datasets: [{
1313
+ label: 'Tokens',
1314
+ data: daily.days.map(d => d.tokens),
1315
+ backgroundColor: '#f97316',
1316
+ yAxisID: 'y',
1317
+ }, {
1318
+ label: 'Cost ($)',
1319
+ data: daily.days.map(d => d.cost_usd),
1320
+ backgroundColor: '#eab308',
1321
+ yAxisID: 'y1',
1322
+ }]
1323
+ },
1324
+ options: {
1325
+ responsive: true,
1326
+ maintainAspectRatio: false,
1327
+ plugins: { legend: { labels: { color: '#94a3b8' } } },
1328
+ scales: {
1329
+ x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
1330
+ y: { type: 'linear', position: 'left', ticks: { color: '#f97316' }, grid: { color: '#334155' } },
1331
+ y1: { type: 'linear', position: 'right', ticks: { color: '#eab308' }, grid: { display: false } },
1332
+ }
1333
+ }
1334
+ });
1335
+ }
1336
+
1337
+ function renderRecentSessions() {
1338
+ const sessions = state.data.sessions?.sessions?.slice(0, 5) || [];
1339
+ if (!sessions.length) {
1340
+ document.getElementById('recent-sessions').innerHTML = '<div class="empty">No sessions found</div>';
1341
+ return;
1342
+ }
1343
+
1344
+ document.getElementById('recent-sessions').innerHTML = `
1345
+ <table class="sessions-table">
1346
+ <thead>
1347
+ <tr>
1348
+ <th>Name</th>
1349
+ <th>Date</th>
1350
+ <th>Tokens</th>
1351
+ <th>Cost</th>
1352
+ <th>Green %</th>
1353
+ </tr>
1354
+ </thead>
1355
+ <tbody>
1356
+ ${sessions.map(s => `
1357
+ <tr>
1358
+ <td class="session-name">
1359
+ ${s.name}
1360
+ <span class="session-badge ${s.is_archived ? '' : 'active'}">${s.is_archived ? 'archived' : 'active'}</span>
1361
+ </td>
1362
+ <td>${formatDate(s.start_time)}</td>
1363
+ <td>${formatTokens(s.total_tokens)}</td>
1364
+ <td>$${s.estimated_cost_usd.toFixed(2)}</td>
1365
+ <td style="color: ${s.green_percent > 50 ? 'var(--green)' : 'var(--yellow)'}">${s.green_percent}%</td>
1366
+ </tr>
1367
+ `).join('')}
1368
+ </tbody>
1369
+ </table>
1370
+ `;
1371
+ }
1372
+
1373
+ function renderSessions() {
1374
+ const showAll = document.getElementById('show-all-sessions').checked;
1375
+ let sessions = state.data.sessions?.sessions || [];
1376
+
1377
+ // Sort
1378
+ sessions = [...sessions].sort((a, b) => {
1379
+ let av = a[state.sortColumn], bv = b[state.sortColumn];
1380
+ if (typeof av === 'string') return state.sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
1381
+ return state.sortAsc ? av - bv : bv - av;
1382
+ });
1383
+
1384
+ const container = document.getElementById('sessions-table-container');
1385
+ if (!sessions.length) {
1386
+ container.innerHTML = '<div class="empty">No sessions found for this time range</div>';
1387
+ return;
1388
+ }
1389
+
1390
+ const columns = [
1391
+ { key: 'name', label: 'Name' },
1392
+ { key: 'start_time', label: 'Date' },
1393
+ { key: 'green_time_seconds', label: 'Duration' },
1394
+ { key: 'total_tokens', label: 'Tokens' },
1395
+ { key: 'estimated_cost_usd', label: 'Cost' },
1396
+ { key: 'green_percent', label: 'Green %' },
1397
+ { key: 'interaction_count', label: 'Interactions' },
1398
+ { key: 'steers_count', label: 'Steers' },
1399
+ ];
1400
+
1401
+ container.innerHTML = `
1402
+ <table class="sessions-table">
1403
+ <thead>
1404
+ <tr>
1405
+ ${columns.map(c => `
1406
+ <th class="${state.sortColumn === c.key ? 'sorted' : ''}"
1407
+ data-column="${c.key}">
1408
+ ${c.label}
1409
+ <span class="sort-icon">${state.sortColumn === c.key ? (state.sortAsc ? '↑' : '↓') : ''}</span>
1410
+ </th>
1411
+ `).join('')}
1412
+ </tr>
1413
+ </thead>
1414
+ <tbody>
1415
+ ${sessions.map(s => `
1416
+ <tr class="expandable-row" data-id="${s.id}">
1417
+ <td class="session-name">
1418
+ ${s.name}
1419
+ <span class="session-badge ${s.is_archived ? '' : 'active'}">${s.is_archived ? 'archived' : 'active'}</span>
1420
+ </td>
1421
+ <td>${formatDate(s.start_time)}</td>
1422
+ <td>${formatDuration(s.green_time_seconds + s.non_green_time_seconds)}</td>
1423
+ <td>${formatTokens(s.total_tokens)}</td>
1424
+ <td>$${s.estimated_cost_usd.toFixed(2)}</td>
1425
+ <td style="color: ${s.green_percent > 50 ? 'var(--green)' : 'var(--yellow)'}">${s.green_percent}%</td>
1426
+ <td>${s.interaction_count}</td>
1427
+ <td>${s.steers_count}</td>
1428
+ </tr>
1429
+ `).join('')}
1430
+ </tbody>
1431
+ </table>
1432
+ `;
1433
+
1434
+ // Sort handlers
1435
+ container.querySelectorAll('th[data-column]').forEach(th => {
1436
+ th.addEventListener('click', () => {
1437
+ const col = th.dataset.column;
1438
+ if (state.sortColumn === col) state.sortAsc = !state.sortAsc;
1439
+ else { state.sortColumn = col; state.sortAsc = true; }
1440
+ renderSessions();
1441
+ });
1442
+ });
1443
+ }
1444
+
1445
+ function renderTimeline() {
1446
+ const timeline = state.data.timeline;
1447
+ if (!timeline?.agents || !Object.keys(timeline.agents).length) {
1448
+ document.getElementById('timeline-content').innerHTML = '<div class="empty">No timeline data available</div>';
1449
+ return;
1450
+ }
1451
+
1452
+ const start = new Date(timeline.start).getTime();
1453
+ const end = new Date(timeline.end).getTime();
1454
+ const range = end - start;
1455
+
1456
+ let html = '';
1457
+ for (const [agent, events] of Object.entries(timeline.agents)) {
1458
+ html += `<div class="timeline-row">
1459
+ <div class="timeline-label">${agent}</div>
1460
+ <div class="timeline-bar">
1461
+ ${renderTimelineSegments(events, start, range)}
1462
+ </div>
1463
+ </div>`;
1464
+ }
1465
+ document.getElementById('timeline-content').innerHTML = html;
1466
+
1467
+ // Presence timeline
1468
+ if (timeline.presence?.length) {
1469
+ document.getElementById('presence-timeline').innerHTML = `
1470
+ <div class="timeline-row">
1471
+ <div class="timeline-label">User</div>
1472
+ <div class="timeline-bar">
1473
+ ${renderPresenceSegments(timeline.presence, start, range)}
1474
+ </div>
1475
+ </div>
1476
+ `;
1477
+ }
1478
+ }
1479
+
1480
+ function renderTimelineSegments(events, start, range) {
1481
+ if (!events.length) return '';
1482
+
1483
+ let html = '';
1484
+ for (let i = 0; i < events.length; i++) {
1485
+ const e = events[i];
1486
+ const eTime = new Date(e.timestamp).getTime();
1487
+ const left = ((eTime - start) / range) * 100;
1488
+ const nextTime = events[i + 1] ? new Date(events[i + 1].timestamp).getTime() : start + range;
1489
+ const width = Math.max(0.5, ((nextTime - eTime) / range) * 100);
1490
+
1491
+ html += `<div class="timeline-segment" style="left:${left}%;width:${width}%;background:${e.color}" title="${e.status}: ${e.activity}"></div>`;
1492
+ }
1493
+ return html;
1494
+ }
1495
+
1496
+ function renderPresenceSegments(events, start, range) {
1497
+ if (!events.length) return '';
1498
+
1499
+ let html = '';
1500
+ for (let i = 0; i < events.length; i++) {
1501
+ const e = events[i];
1502
+ const eTime = new Date(e.timestamp).getTime();
1503
+ const left = ((eTime - start) / range) * 100;
1504
+ const nextTime = events[i + 1] ? new Date(events[i + 1].timestamp).getTime() : start + range;
1505
+ const width = Math.max(0.5, ((nextTime - eTime) / range) * 100);
1506
+
1507
+ html += `<div class="timeline-segment" style="left:${left}%;width:${width}%;background:${e.color}" title="${e.state_name}"></div>`;
1508
+ }
1509
+ return html;
1510
+ }
1511
+
1512
+ function renderEfficiency() {
1513
+ const stats = state.data.stats;
1514
+ if (!stats) return;
1515
+
1516
+ const s = stats.summary || {};
1517
+ const e = stats.efficiency || {};
1518
+ const i = stats.interactions || {};
1519
+ const w = stats.work_times || {};
1520
+ const p = stats.presence_efficiency || {};
1521
+
1522
+ document.getElementById('efficiency-summary').innerHTML = `
1523
+ <div class="summary-card green">
1524
+ <div class="summary-value">${e.green_percent || 0}%</div>
1525
+ <div class="summary-label">Green Time</div>
1526
+ </div>
1527
+ <div class="summary-card orange">
1528
+ <div class="summary-value">$${e.cost_per_hour || 0}</div>
1529
+ <div class="summary-label">Cost/Hour</div>
1530
+ </div>
1531
+ <div class="summary-card yellow">
1532
+ <div class="summary-value">${e.spin_rate_percent || 0}%</div>
1533
+ <div class="summary-label">Spin Rate</div>
1534
+ </div>
1535
+ <div class="summary-card cyan">
1536
+ <div class="summary-value">${formatDuration(w.median || 0)}</div>
1537
+ <div class="summary-label">Median Work Time</div>
1538
+ </div>
1539
+ `;
1540
+
1541
+ // Presence efficiency metrics
1542
+ const presenceHasData = p.has_data;
1543
+ const presenceContent = presenceHasData ? `
1544
+ <div class="metric-value" style="color: var(--green)">${p.present_efficiency || 0}%</div>
1545
+ <div class="metric-sub">Green while you're active</div>
1546
+ <table class="work-times-table">
1547
+ <tr><td>AFK efficiency</td><td style="color: var(--yellow)">${p.afk_efficiency || 0}%</td></tr>
1548
+ <tr><td>Present samples</td><td>${p.present_samples || 0}</td></tr>
1549
+ <tr><td>AFK samples</td><td>${p.afk_samples || 0}</td></tr>
1550
+ </table>
1551
+ ` : `
1552
+ <div class="metric-value dim">—</div>
1553
+ <div class="metric-sub">No presence data available</div>
1554
+ <div style="color: var(--text-muted); font-size: 12px; margin-top: 8px;">
1555
+ Install presence tracking:<br>
1556
+ <code style="color: var(--cyan)">pip install overcode[presence]</code>
1557
+ </div>
1558
+ `;
1559
+ document.getElementById('presence-efficiency-metrics').innerHTML = presenceContent;
1560
+
1561
+ document.getElementById('cost-metrics').innerHTML = `
1562
+ <div class="metric-value">$${s.total_cost_usd || 0}</div>
1563
+ <div class="metric-sub">Total cost for period</div>
1564
+ <table class="work-times-table">
1565
+ <tr><td>Per interaction</td><td>$${e.cost_per_interaction || 0}</td></tr>
1566
+ <tr><td>Per hour</td><td>$${e.cost_per_hour || 0}</td></tr>
1567
+ <tr><td>Total tokens</td><td>${formatTokens(s.total_tokens || 0)}</td></tr>
1568
+ </table>
1569
+ `;
1570
+
1571
+ document.getElementById('work-time-metrics').innerHTML = `
1572
+ <div class="metric-value">${formatDuration(w.median || 0)}</div>
1573
+ <div class="metric-sub">Median work cycle time</div>
1574
+ <table class="work-times-table">
1575
+ <tr><td>Mean</td><td>${formatDuration(w.mean || 0)}</td></tr>
1576
+ <tr><td>P5</td><td>${formatDuration(w.p5 || 0)}</td></tr>
1577
+ <tr><td>P95</td><td>${formatDuration(w.p95 || 0)}</td></tr>
1578
+ <tr><td>Min</td><td>${formatDuration(w.min || 0)}</td></tr>
1579
+ <tr><td>Max</td><td>${formatDuration(w.max || 0)}</td></tr>
1580
+ </table>
1581
+ `;
1582
+
1583
+ document.getElementById('interaction-metrics').innerHTML = `
1584
+ <div class="metric-value">${i.total || 0}</div>
1585
+ <div class="metric-sub">Total interactions</div>
1586
+ <table class="work-times-table">
1587
+ <tr><td>Human</td><td>${i.human || 0}</td></tr>
1588
+ <tr><td>Robot steers</td><td>${i.robot_steers || 0}</td></tr>
1589
+ <tr><td>Spin rate</td><td>${e.spin_rate_percent || 0}%</td></tr>
1590
+ </table>
1591
+ `;
1592
+
1593
+ renderEfficiencyChart();
1594
+ }
1595
+
1596
+ function renderEfficiencyChart() {
1597
+ const daily = state.data.daily;
1598
+ if (!daily?.days?.length) return;
1599
+
1600
+ const ctx = document.getElementById('efficiency-chart').getContext('2d');
1601
+ if (state.charts.efficiency) state.charts.efficiency.destroy();
1602
+
1603
+ state.charts.efficiency = new Chart(ctx, {
1604
+ type: 'line',
1605
+ data: {
1606
+ labels: daily.labels,
1607
+ datasets: [{
1608
+ label: 'Green %',
1609
+ data: daily.days.map(d => d.green_percent),
1610
+ borderColor: '#22c55e',
1611
+ backgroundColor: 'rgba(34, 197, 94, 0.1)',
1612
+ fill: true,
1613
+ tension: 0.3,
1614
+ }]
1615
+ },
1616
+ options: {
1617
+ responsive: true,
1618
+ maintainAspectRatio: false,
1619
+ plugins: { legend: { labels: { color: '#94a3b8' } } },
1620
+ scales: {
1621
+ x: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
1622
+ y: { min: 0, max: 100, ticks: { color: '#22c55e' }, grid: { color: '#334155' } },
1623
+ }
1624
+ }
1625
+ });
1626
+ }
1627
+
1628
+ // Helpers
1629
+ function formatTokens(n) {
1630
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1631
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1632
+ return n.toString();
1633
+ }
1634
+
1635
+ function formatDuration(seconds) {
1636
+ if (!seconds) return '-';
1637
+ if (seconds < 60) return Math.round(seconds) + 's';
1638
+ if (seconds < 3600) return Math.round(seconds / 60) + 'm';
1639
+ return (seconds / 3600).toFixed(1) + 'h';
1640
+ }
1641
+
1642
+ function formatDate(iso) {
1643
+ if (!iso) return '-';
1644
+ const d = new Date(iso);
1645
+ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1646
+ }
1647
+
1648
+ // Event handlers
1649
+ document.getElementById('show-all-sessions').addEventListener('change', renderSessions);
1650
+ window.addEventListener('hashchange', loadFromHash);
1651
+
1652
+ // Start
1653
+ init();
1654
+ </script>
1655
+ </body>
1656
+ </html>"""