zozul-cli 0.2.4 → 0.2.5

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.
@@ -104,6 +104,13 @@
104
104
  }
105
105
  .nav-tab:hover { color: var(--text); }
106
106
  .nav-tab.active { color: var(--accent-light); border-bottom-color: var(--accent); }
107
+ .nav-divider {
108
+ width: 1px;
109
+ height: 20px;
110
+ background: var(--border);
111
+ align-self: center;
112
+ margin: 0 8px;
113
+ }
107
114
 
108
115
  .container { max-width: 1280px; margin: 0 auto; padding: 24px; }
109
116
 
@@ -440,6 +447,8 @@
440
447
  <button class="nav-tab" data-view="tasks">Tasks</button>
441
448
  <button class="nav-tab" data-view="tags">Tags</button>
442
449
  <button class="nav-tab" data-view="sessions">Sessions</button>
450
+ <span class="nav-divider" id="team-divider" style="display:none"></span>
451
+ <button class="nav-tab" data-view="team" id="team-tab" style="display:none">Team</button>
443
452
  </div>
444
453
 
445
454
  <div class="container">
@@ -453,6 +462,40 @@
453
462
  </div>
454
463
  </div>
455
464
 
465
+ <!-- Team View -->
466
+ <div id="view-team" class="view">
467
+ <div style="display:flex;align-items:center;gap:16px;margin-bottom:24px">
468
+ <select id="team-selector" class="panel-filter" style="width:auto" onchange="onTeamSelect()"></select>
469
+ <div class="time-selector" style="margin-bottom:0"></div>
470
+ </div>
471
+ <div class="stats-grid" id="team-stats"></div>
472
+ <div class="panel">
473
+ <div class="panel-header"><span>Members</span></div>
474
+ <table>
475
+ <thead><tr>
476
+ <th class="sortable" data-sort="team-members:user_name">Name</th>
477
+ <th class="sortable" data-sort="team-members:total_sessions">Sessions</th>
478
+ <th class="sortable desc" data-sort="team-members:total_cost_usd">Cost</th>
479
+ <th class="sortable" data-sort="team-members:total_duration_ms">Active Time</th>
480
+ </tr></thead>
481
+ <tbody id="team-members-table"></tbody>
482
+ </table>
483
+ </div>
484
+ <div class="panel">
485
+ <div class="panel-header"><span>Team Tasks</span></div>
486
+ <table>
487
+ <thead><tr>
488
+ <th class="sortable" data-sort="team-tasks:tags">Task</th>
489
+ <th class="sortable" data-sort="team-tasks:total_duration_ms">Time</th>
490
+ <th class="sortable desc" data-sort="team-tasks:total_cost_usd">Cost</th>
491
+ <th class="sortable" data-sort="team-tasks:human_interventions">Human Interventions</th>
492
+ </tr></thead>
493
+ <tbody id="team-tasks-table"></tbody>
494
+ </table>
495
+ <div id="team-tasks-pagination" style="display:flex;gap:6px;align-items:center;justify-content:center;padding:12px 16px;border-top:1px solid var(--border)"></div>
496
+ </div>
497
+ </div>
498
+
456
499
  <!-- Sessions View -->
457
500
  <div id="view-sessions" class="view">
458
501
  <div class="panel">
@@ -598,7 +641,12 @@ let sortState = {
598
641
  tasks: { key: 'total_cost_usd', dir: 'desc' },
599
642
  tags: { key: 'total_cost_usd', dir: 'desc' },
600
643
  sessions: { key: 'started_at', dir: 'desc' },
644
+ 'team-members': { key: 'total_cost_usd', dir: 'desc' },
645
+ 'team-tasks': { key: 'total_cost_usd', dir: 'desc' },
601
646
  };
647
+ let teamData = null;
648
+ let allTeamMembers = [];
649
+ let allTeamTasks = [];
602
650
  let allSessions = [];
603
651
  let sessionsTotal = 0;
604
652
  let sessionsOffset = 0;
@@ -766,6 +814,16 @@ function copyText(text) {
766
814
  });
767
815
  }
768
816
 
817
+ function timeQueryString() {
818
+ if (currentTimeWindow === 'all') return '';
819
+ const now = new Date();
820
+ const ms = currentTimeWindow.endsWith('d')
821
+ ? parseInt(currentTimeWindow) * 86400000
822
+ : parseInt(currentTimeWindow) * 3600000;
823
+ const from = new Date(now.getTime() - ms).toISOString();
824
+ return 'from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(now.toISOString());
825
+ }
826
+
769
827
  // ── Navigation ──
770
828
 
771
829
  function showView(name) {
@@ -776,7 +834,7 @@ function showView(name) {
776
834
 
777
835
  // Update tab highlight for main views
778
836
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
779
- const mainView = ['summary', 'sessions', 'tasks', 'tags'].includes(name) ? name : null;
837
+ const mainView = ['summary', 'sessions', 'tasks', 'tags', 'team'].includes(name) ? name : null;
780
838
  if (mainView) {
781
839
  const tab = document.querySelector('.nav-tab[data-view="' + mainView + '"]');
782
840
  if (tab) tab.classList.add('active');
@@ -821,6 +879,8 @@ document.addEventListener('click', e => {
821
879
  if (table === 'tasks') renderTaskTable(sortData(allTaskGroups, state));
822
880
  else if (table === 'tags') renderTagTable(sortData(allTagStats, state));
823
881
  else if (table === 'sessions') renderSessionsTable(sortData(allSessions, state));
882
+ else if (table === 'team-members') renderTeamMembers(sortData(allTeamMembers, state));
883
+ else if (table === 'team-tasks') renderTeamTasks(sortData(allTeamTasks, state));
824
884
  return;
825
885
  }
826
886
  });
@@ -1476,6 +1536,117 @@ function renderSessionsTable(sessions) {
1476
1536
  }).join('');
1477
1537
  }
1478
1538
 
1539
+ // ── Team View ──
1540
+
1541
+ async function checkTeamAccess() {
1542
+ try {
1543
+ const data = await fetchJson('/api/teams/stats');
1544
+ if (data && data.team_id) {
1545
+ teamData = data;
1546
+ document.getElementById('team-tab').style.display = '';
1547
+ document.getElementById('team-divider').style.display = '';
1548
+ // Populate selector (single team for now, extensible)
1549
+ const sel = document.getElementById('team-selector');
1550
+ sel.innerHTML = '<option value="' + data.team_id + '">' + escHtml(data.team_name) + '</option>';
1551
+ }
1552
+ } catch {
1553
+ // Not a manager or no team — hide tab
1554
+ document.getElementById('team-tab').style.display = 'none';
1555
+ }
1556
+ }
1557
+
1558
+ function onTeamSelect() { loadTeam(); }
1559
+
1560
+ async function loadTeam() {
1561
+ try {
1562
+ const timeQs = timeQueryString();
1563
+ const timeParam = timeQs ? '?' + timeQs : '';
1564
+ const [stats, taskGroups] = await Promise.all([
1565
+ fetchJson('/api/teams/stats' + timeParam),
1566
+ fetchJson('/api/teams/task-groups' + timeParam),
1567
+ ]);
1568
+ teamData = stats;
1569
+ renderTeamStats(stats);
1570
+ allTeamMembers = stats.members || [];
1571
+ renderTeamMembers(sortData(allTeamMembers, sortState['team-members']));
1572
+ allTeamTasks = taskGroups || [];
1573
+ teamTasksPage = 0;
1574
+ renderTeamTasks(sortData(allTeamTasks, sortState['team-tasks']));
1575
+ } catch (e) {
1576
+ console.error('team load failed', e);
1577
+ }
1578
+ }
1579
+
1580
+ function renderTeamStats(s) {
1581
+ const grid = document.getElementById('team-stats');
1582
+ grid.innerHTML = [
1583
+ { label: 'Total Cost', value: fmtCost(s.total_cost_usd), hero: true },
1584
+ { label: 'Sessions', value: fmtNum(s.total_sessions), hero: true },
1585
+ { label: 'Members', value: fmtNum((s.members || []).length), hero: true },
1586
+ ].map(c =>
1587
+ '<div class="stat-card' + (c.hero ? ' hero' : '') + '">' +
1588
+ '<div class="label">' + c.label + '</div>' +
1589
+ '<div class="value">' + c.value + '</div>' +
1590
+ '</div>'
1591
+ ).join('');
1592
+ }
1593
+
1594
+ function renderTeamMembers(members) {
1595
+ const tbody = document.getElementById('team-members-table');
1596
+ if (!members.length) {
1597
+ tbody.innerHTML = '<tr><td colspan="4" class="empty">No members.</td></tr>';
1598
+ return;
1599
+ }
1600
+ const maxCost = Math.max(...members.map(m => m.total_cost_usd || 0), 1);
1601
+ tbody.innerHTML = members.map(m => {
1602
+ const pct = Math.round((m.total_cost_usd || 0) / maxCost * 100);
1603
+ return '<tr>' +
1604
+ '<td>' + escHtml(m.user_name || 'Unknown') + '</td>' +
1605
+ '<td>' + fmtNum(m.total_sessions) + '</td>' +
1606
+ '<td><div style="display:flex;align-items:center;gap:10px">' +
1607
+ '<div class="project-bar-wrap"><div class="project-bar" style="width:' + pct + '%"></div></div>' +
1608
+ '<span class="badge badge-cost">' + fmtCost(m.total_cost_usd) + '</span>' +
1609
+ '</div></td>' +
1610
+ '<td>' + fmtDuration(m.total_duration_ms) + '</td>' +
1611
+ '</tr>';
1612
+ }).join('');
1613
+ }
1614
+
1615
+ let teamTasksPage = 0;
1616
+ const TEAM_TASKS_PAGE = 10;
1617
+
1618
+ function renderTeamTasks(tasks) {
1619
+ const tbody = document.getElementById('team-tasks-table');
1620
+ const pag = document.getElementById('team-tasks-pagination');
1621
+ if (!tasks.length) {
1622
+ tbody.innerHTML = '<tr><td colspan="4" class="empty">No tasks.</td></tr>';
1623
+ pag.innerHTML = '';
1624
+ return;
1625
+ }
1626
+ const totalPages = Math.ceil(tasks.length / TEAM_TASKS_PAGE);
1627
+ teamTasksPage = Math.min(teamTasksPage, totalPages - 1);
1628
+ const start = teamTasksPage * TEAM_TASKS_PAGE;
1629
+ const page = tasks.slice(start, start + TEAM_TASKS_PAGE);
1630
+
1631
+ tbody.innerHTML = page.map(g => {
1632
+ const tags = g.tags.split('|');
1633
+ const pills = tags.map(t =>
1634
+ '<span class="tag-pill' + (t === 'Untagged' ? ' untagged' : '') + '">' + escHtml(t) + '</span>'
1635
+ ).join('');
1636
+ return '<tr>' +
1637
+ '<td>' + pills + '</td>' +
1638
+ '<td>' + fmtDuration(g.total_duration_ms) + '</td>' +
1639
+ '<td><span class="badge badge-cost">' + fmtCost(g.total_cost_usd) + '</span></td>' +
1640
+ '<td>' + fmtNum(g.human_interventions) + '</td>' +
1641
+ '</tr>';
1642
+ }).join('');
1643
+
1644
+ pag.innerHTML = totalPages <= 1 ? '' :
1645
+ (teamTasksPage > 0 ? '<button class="time-btn" onclick="teamTasksPage--;renderTeamTasks(sortData(allTeamTasks,sortState[\'team-tasks\']))">Prev</button>' : '') +
1646
+ '<span style="font-size:11px;color:var(--text-dim);font-family:var(--mono)">' + (teamTasksPage + 1) + ' / ' + totalPages + '</span>' +
1647
+ (teamTasksPage < totalPages - 1 ? '<button class="time-btn" onclick="teamTasksPage++;renderTeamTasks(sortData(allTeamTasks,sortState[\'team-tasks\']))">Next</button>' : '');
1648
+ }
1649
+
1479
1650
  // ── Loading orchestration ──
1480
1651
 
1481
1652
  async function loadViewData(view) {
@@ -1483,6 +1654,7 @@ async function loadViewData(view) {
1483
1654
  else if (view === 'sessions') await loadSessions();
1484
1655
  else if (view === 'tasks') await loadTasks();
1485
1656
  else if (view === 'tags') await loadTags();
1657
+ else if (view === 'team') await loadTeam();
1486
1658
  }
1487
1659
 
1488
1660
  async function loadDashboard() {
@@ -1507,7 +1679,7 @@ function manualRefresh() {
1507
1679
  function scheduleAutoRefresh() {
1508
1680
  autoRefreshTimer = setTimeout(async () => {
1509
1681
  // Only auto-refresh main views, not detail views
1510
- if (['summary', 'sessions', 'tasks', 'tags'].includes(currentView)) {
1682
+ if (['summary', 'sessions', 'tasks', 'tags', 'team'].includes(currentView)) {
1511
1683
  await loadDashboard();
1512
1684
  }
1513
1685
  scheduleAutoRefresh();
@@ -1516,7 +1688,10 @@ function scheduleAutoRefresh() {
1516
1688
 
1517
1689
  // ── Init ──
1518
1690
 
1519
- detectDataSource().then(() => loadDashboard()).then(scheduleAutoRefresh);
1691
+ detectDataSource().then(() => {
1692
+ checkTeamAccess();
1693
+ return loadDashboard();
1694
+ }).then(scheduleAutoRefresh);
1520
1695
  </script>
1521
1696
  </body>
1522
1697
  </html>
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "zozul-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Observability for Claude Code — track token usage, costs, turns, and conversation history",
5
+ "readmeFilename": "README.md",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "bin": {
@@ -104,6 +104,13 @@
104
104
  }
105
105
  .nav-tab:hover { color: var(--text); }
106
106
  .nav-tab.active { color: var(--accent-light); border-bottom-color: var(--accent); }
107
+ .nav-divider {
108
+ width: 1px;
109
+ height: 20px;
110
+ background: var(--border);
111
+ align-self: center;
112
+ margin: 0 8px;
113
+ }
107
114
 
108
115
  .container { max-width: 1280px; margin: 0 auto; padding: 24px; }
109
116
 
@@ -440,6 +447,8 @@
440
447
  <button class="nav-tab" data-view="tasks">Tasks</button>
441
448
  <button class="nav-tab" data-view="tags">Tags</button>
442
449
  <button class="nav-tab" data-view="sessions">Sessions</button>
450
+ <span class="nav-divider" id="team-divider" style="display:none"></span>
451
+ <button class="nav-tab" data-view="team" id="team-tab" style="display:none">Team</button>
443
452
  </div>
444
453
 
445
454
  <div class="container">
@@ -453,6 +462,40 @@
453
462
  </div>
454
463
  </div>
455
464
 
465
+ <!-- Team View -->
466
+ <div id="view-team" class="view">
467
+ <div style="display:flex;align-items:center;gap:16px;margin-bottom:24px">
468
+ <select id="team-selector" class="panel-filter" style="width:auto" onchange="onTeamSelect()"></select>
469
+ <div class="time-selector" style="margin-bottom:0"></div>
470
+ </div>
471
+ <div class="stats-grid" id="team-stats"></div>
472
+ <div class="panel">
473
+ <div class="panel-header"><span>Members</span></div>
474
+ <table>
475
+ <thead><tr>
476
+ <th class="sortable" data-sort="team-members:user_name">Name</th>
477
+ <th class="sortable" data-sort="team-members:total_sessions">Sessions</th>
478
+ <th class="sortable desc" data-sort="team-members:total_cost_usd">Cost</th>
479
+ <th class="sortable" data-sort="team-members:total_duration_ms">Active Time</th>
480
+ </tr></thead>
481
+ <tbody id="team-members-table"></tbody>
482
+ </table>
483
+ </div>
484
+ <div class="panel">
485
+ <div class="panel-header"><span>Team Tasks</span></div>
486
+ <table>
487
+ <thead><tr>
488
+ <th class="sortable" data-sort="team-tasks:tags">Task</th>
489
+ <th class="sortable" data-sort="team-tasks:total_duration_ms">Time</th>
490
+ <th class="sortable desc" data-sort="team-tasks:total_cost_usd">Cost</th>
491
+ <th class="sortable" data-sort="team-tasks:human_interventions">Human Interventions</th>
492
+ </tr></thead>
493
+ <tbody id="team-tasks-table"></tbody>
494
+ </table>
495
+ <div id="team-tasks-pagination" style="display:flex;gap:6px;align-items:center;justify-content:center;padding:12px 16px;border-top:1px solid var(--border)"></div>
496
+ </div>
497
+ </div>
498
+
456
499
  <!-- Sessions View -->
457
500
  <div id="view-sessions" class="view">
458
501
  <div class="panel">
@@ -598,7 +641,12 @@ let sortState = {
598
641
  tasks: { key: 'total_cost_usd', dir: 'desc' },
599
642
  tags: { key: 'total_cost_usd', dir: 'desc' },
600
643
  sessions: { key: 'started_at', dir: 'desc' },
644
+ 'team-members': { key: 'total_cost_usd', dir: 'desc' },
645
+ 'team-tasks': { key: 'total_cost_usd', dir: 'desc' },
601
646
  };
647
+ let teamData = null;
648
+ let allTeamMembers = [];
649
+ let allTeamTasks = [];
602
650
  let allSessions = [];
603
651
  let sessionsTotal = 0;
604
652
  let sessionsOffset = 0;
@@ -766,6 +814,16 @@ function copyText(text) {
766
814
  });
767
815
  }
768
816
 
817
+ function timeQueryString() {
818
+ if (currentTimeWindow === 'all') return '';
819
+ const now = new Date();
820
+ const ms = currentTimeWindow.endsWith('d')
821
+ ? parseInt(currentTimeWindow) * 86400000
822
+ : parseInt(currentTimeWindow) * 3600000;
823
+ const from = new Date(now.getTime() - ms).toISOString();
824
+ return 'from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(now.toISOString());
825
+ }
826
+
769
827
  // ── Navigation ──
770
828
 
771
829
  function showView(name) {
@@ -776,7 +834,7 @@ function showView(name) {
776
834
 
777
835
  // Update tab highlight for main views
778
836
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
779
- const mainView = ['summary', 'sessions', 'tasks', 'tags'].includes(name) ? name : null;
837
+ const mainView = ['summary', 'sessions', 'tasks', 'tags', 'team'].includes(name) ? name : null;
780
838
  if (mainView) {
781
839
  const tab = document.querySelector('.nav-tab[data-view="' + mainView + '"]');
782
840
  if (tab) tab.classList.add('active');
@@ -821,6 +879,8 @@ document.addEventListener('click', e => {
821
879
  if (table === 'tasks') renderTaskTable(sortData(allTaskGroups, state));
822
880
  else if (table === 'tags') renderTagTable(sortData(allTagStats, state));
823
881
  else if (table === 'sessions') renderSessionsTable(sortData(allSessions, state));
882
+ else if (table === 'team-members') renderTeamMembers(sortData(allTeamMembers, state));
883
+ else if (table === 'team-tasks') renderTeamTasks(sortData(allTeamTasks, state));
824
884
  return;
825
885
  }
826
886
  });
@@ -1476,6 +1536,117 @@ function renderSessionsTable(sessions) {
1476
1536
  }).join('');
1477
1537
  }
1478
1538
 
1539
+ // ── Team View ──
1540
+
1541
+ async function checkTeamAccess() {
1542
+ try {
1543
+ const data = await fetchJson('/api/teams/stats');
1544
+ if (data && data.team_id) {
1545
+ teamData = data;
1546
+ document.getElementById('team-tab').style.display = '';
1547
+ document.getElementById('team-divider').style.display = '';
1548
+ // Populate selector (single team for now, extensible)
1549
+ const sel = document.getElementById('team-selector');
1550
+ sel.innerHTML = '<option value="' + data.team_id + '">' + escHtml(data.team_name) + '</option>';
1551
+ }
1552
+ } catch {
1553
+ // Not a manager or no team — hide tab
1554
+ document.getElementById('team-tab').style.display = 'none';
1555
+ }
1556
+ }
1557
+
1558
+ function onTeamSelect() { loadTeam(); }
1559
+
1560
+ async function loadTeam() {
1561
+ try {
1562
+ const timeQs = timeQueryString();
1563
+ const timeParam = timeQs ? '?' + timeQs : '';
1564
+ const [stats, taskGroups] = await Promise.all([
1565
+ fetchJson('/api/teams/stats' + timeParam),
1566
+ fetchJson('/api/teams/task-groups' + timeParam),
1567
+ ]);
1568
+ teamData = stats;
1569
+ renderTeamStats(stats);
1570
+ allTeamMembers = stats.members || [];
1571
+ renderTeamMembers(sortData(allTeamMembers, sortState['team-members']));
1572
+ allTeamTasks = taskGroups || [];
1573
+ teamTasksPage = 0;
1574
+ renderTeamTasks(sortData(allTeamTasks, sortState['team-tasks']));
1575
+ } catch (e) {
1576
+ console.error('team load failed', e);
1577
+ }
1578
+ }
1579
+
1580
+ function renderTeamStats(s) {
1581
+ const grid = document.getElementById('team-stats');
1582
+ grid.innerHTML = [
1583
+ { label: 'Total Cost', value: fmtCost(s.total_cost_usd), hero: true },
1584
+ { label: 'Sessions', value: fmtNum(s.total_sessions), hero: true },
1585
+ { label: 'Members', value: fmtNum((s.members || []).length), hero: true },
1586
+ ].map(c =>
1587
+ '<div class="stat-card' + (c.hero ? ' hero' : '') + '">' +
1588
+ '<div class="label">' + c.label + '</div>' +
1589
+ '<div class="value">' + c.value + '</div>' +
1590
+ '</div>'
1591
+ ).join('');
1592
+ }
1593
+
1594
+ function renderTeamMembers(members) {
1595
+ const tbody = document.getElementById('team-members-table');
1596
+ if (!members.length) {
1597
+ tbody.innerHTML = '<tr><td colspan="4" class="empty">No members.</td></tr>';
1598
+ return;
1599
+ }
1600
+ const maxCost = Math.max(...members.map(m => m.total_cost_usd || 0), 1);
1601
+ tbody.innerHTML = members.map(m => {
1602
+ const pct = Math.round((m.total_cost_usd || 0) / maxCost * 100);
1603
+ return '<tr>' +
1604
+ '<td>' + escHtml(m.user_name || 'Unknown') + '</td>' +
1605
+ '<td>' + fmtNum(m.total_sessions) + '</td>' +
1606
+ '<td><div style="display:flex;align-items:center;gap:10px">' +
1607
+ '<div class="project-bar-wrap"><div class="project-bar" style="width:' + pct + '%"></div></div>' +
1608
+ '<span class="badge badge-cost">' + fmtCost(m.total_cost_usd) + '</span>' +
1609
+ '</div></td>' +
1610
+ '<td>' + fmtDuration(m.total_duration_ms) + '</td>' +
1611
+ '</tr>';
1612
+ }).join('');
1613
+ }
1614
+
1615
+ let teamTasksPage = 0;
1616
+ const TEAM_TASKS_PAGE = 10;
1617
+
1618
+ function renderTeamTasks(tasks) {
1619
+ const tbody = document.getElementById('team-tasks-table');
1620
+ const pag = document.getElementById('team-tasks-pagination');
1621
+ if (!tasks.length) {
1622
+ tbody.innerHTML = '<tr><td colspan="4" class="empty">No tasks.</td></tr>';
1623
+ pag.innerHTML = '';
1624
+ return;
1625
+ }
1626
+ const totalPages = Math.ceil(tasks.length / TEAM_TASKS_PAGE);
1627
+ teamTasksPage = Math.min(teamTasksPage, totalPages - 1);
1628
+ const start = teamTasksPage * TEAM_TASKS_PAGE;
1629
+ const page = tasks.slice(start, start + TEAM_TASKS_PAGE);
1630
+
1631
+ tbody.innerHTML = page.map(g => {
1632
+ const tags = g.tags.split('|');
1633
+ const pills = tags.map(t =>
1634
+ '<span class="tag-pill' + (t === 'Untagged' ? ' untagged' : '') + '">' + escHtml(t) + '</span>'
1635
+ ).join('');
1636
+ return '<tr>' +
1637
+ '<td>' + pills + '</td>' +
1638
+ '<td>' + fmtDuration(g.total_duration_ms) + '</td>' +
1639
+ '<td><span class="badge badge-cost">' + fmtCost(g.total_cost_usd) + '</span></td>' +
1640
+ '<td>' + fmtNum(g.human_interventions) + '</td>' +
1641
+ '</tr>';
1642
+ }).join('');
1643
+
1644
+ pag.innerHTML = totalPages <= 1 ? '' :
1645
+ (teamTasksPage > 0 ? '<button class="time-btn" onclick="teamTasksPage--;renderTeamTasks(sortData(allTeamTasks,sortState[\'team-tasks\']))">Prev</button>' : '') +
1646
+ '<span style="font-size:11px;color:var(--text-dim);font-family:var(--mono)">' + (teamTasksPage + 1) + ' / ' + totalPages + '</span>' +
1647
+ (teamTasksPage < totalPages - 1 ? '<button class="time-btn" onclick="teamTasksPage++;renderTeamTasks(sortData(allTeamTasks,sortState[\'team-tasks\']))">Next</button>' : '');
1648
+ }
1649
+
1479
1650
  // ── Loading orchestration ──
1480
1651
 
1481
1652
  async function loadViewData(view) {
@@ -1483,6 +1654,7 @@ async function loadViewData(view) {
1483
1654
  else if (view === 'sessions') await loadSessions();
1484
1655
  else if (view === 'tasks') await loadTasks();
1485
1656
  else if (view === 'tags') await loadTags();
1657
+ else if (view === 'team') await loadTeam();
1486
1658
  }
1487
1659
 
1488
1660
  async function loadDashboard() {
@@ -1507,7 +1679,7 @@ function manualRefresh() {
1507
1679
  function scheduleAutoRefresh() {
1508
1680
  autoRefreshTimer = setTimeout(async () => {
1509
1681
  // Only auto-refresh main views, not detail views
1510
- if (['summary', 'sessions', 'tasks', 'tags'].includes(currentView)) {
1682
+ if (['summary', 'sessions', 'tasks', 'tags', 'team'].includes(currentView)) {
1511
1683
  await loadDashboard();
1512
1684
  }
1513
1685
  scheduleAutoRefresh();
@@ -1516,7 +1688,10 @@ function scheduleAutoRefresh() {
1516
1688
 
1517
1689
  // ── Init ──
1518
1690
 
1519
- detectDataSource().then(() => loadDashboard()).then(scheduleAutoRefresh);
1691
+ detectDataSource().then(() => {
1692
+ checkTeamAccess();
1693
+ return loadDashboard();
1694
+ }).then(scheduleAutoRefresh);
1520
1695
  </script>
1521
1696
  </body>
1522
1697
  </html>