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.
- package/dist/dashboard/index.html +178 -3
- package/package.json +2 -1
- package/src/dashboard/index.html +178 -3
|
@@ -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(() =>
|
|
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.
|
|
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": {
|
package/src/dashboard/index.html
CHANGED
|
@@ -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(() =>
|
|
1691
|
+
detectDataSource().then(() => {
|
|
1692
|
+
checkTeamAccess();
|
|
1693
|
+
return loadDashboard();
|
|
1694
|
+
}).then(scheduleAutoRefresh);
|
|
1520
1695
|
</script>
|
|
1521
1696
|
</body>
|
|
1522
1697
|
</html>
|