Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/backend/services/syncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,15 @@ export class SyncService {
shareWorkspaceMachineNames: settings.shareWorkspaceMachineNames
});
if (!sharingPolicy.allowCloudSync || !isConfigured) {
if (!sharingPolicy.allowCloudSync) {
this.deps.log(`Backend sync: not starting timer (cloud sync disabled, profile: ${settings.sharingProfile})`);
} else if (!isConfigured) {
this.deps.log('Backend sync: not starting timer (backend not configured)');
}
return;
}
const intervalMs = Math.max(BACKEND_SYNC_MIN_INTERVAL_MS, settings.lookbackDays * 60 * 1000);
this.deps.log(`Backend sync: starting timer with interval ${intervalMs}ms`);
const intervalMs = BACKEND_SYNC_MIN_INTERVAL_MS;
this.deps.log(`Backend sync: starting timer with interval ${intervalMs}ms (${intervalMs / 60000} minutes)`);
this.backendSyncInterval = setInterval(() => {
this.syncToBackendStore(false, settings, isConfigured).catch((e) => {
this.deps.warn(`Backend sync timer failed: ${e?.message ?? e}`);
Expand Down Expand Up @@ -624,6 +629,11 @@ export class SyncService {
shareWorkspaceMachineNames: settings.shareWorkspaceMachineNames
});
if (!sharingPolicy.allowCloudSync || !isConfigured) {
if (!sharingPolicy.allowCloudSync) {
this.deps.log(`Backend sync: skipping (sharing policy does not allow cloud sync, profile: ${settings.sharingProfile})`);
} else if (!isConfigured) {
this.deps.log('Backend sync: skipping (backend not configured - missing storage account, subscription, or resource group)');
}
return;
}

Expand All @@ -641,6 +651,13 @@ export class SyncService {
const creds = await this.credentialService.getBackendDataPlaneCredentials(settings);
if (!creds) {
// Shared Key mode selected but key not available (or user canceled). Keep local mode functional.
this.deps.warn('Backend sync: skipping (credentials not available - check authentication mode and secrets)');
// Update timestamp to prevent stale "last sync" display
try {
await this.deps.context?.globalState.update('backend.lastSyncAt', Date.now());
} catch (e) {
this.deps.warn(`Backend sync: failed to update lastSyncAt: ${e}`);
}
return;
}
await this.dataPlaneService.ensureTableExists(settings, creds.tableCredential);
Expand Down
58 changes: 53 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,21 +948,28 @@ class CopilotTokenTracker implements vscode.Disposable {
const delaySeconds = process.env.CODESPACES === 'true' ? 5 : 2;
this.log(`⏳ Waiting for Copilot Extension to start (${delaySeconds}s delay)`);

this.initialDelayTimeout = setTimeout(() => {
this.initialDelayTimeout = setTimeout(async () => {
try {
this.log('🚀 Starting token usage analysis...');
this.recheckCopilotExtensionsAfterDelay();
this.updateTokenStats();
await this.updateTokenStats();
this.startBackendSyncAfterInitialAnalysis();
} catch (error) {
this.error('Error in delayed initial update:', error);
}
}, delaySeconds * 1000);
} else if (!copilotExtension && !copilotChatExtension) {
this.log('⚠️ No Copilot extensions found - starting analysis anyway');
setTimeout(() => this.updateTokenStats(), 100);
setTimeout(async () => {
await this.updateTokenStats();
this.startBackendSyncAfterInitialAnalysis();
}, 100);
} else {
this.log('✅ Copilot extensions are active - starting token analysis');
setTimeout(() => this.updateTokenStats(), 100);
setTimeout(async () => {
await this.updateTokenStats();
this.startBackendSyncAfterInitialAnalysis();
}, 100);
}
}

Expand All @@ -980,6 +987,21 @@ class CopilotTokenTracker implements vscode.Disposable {
}
}

/**
* Start backend sync timer after initial token analysis completes.
* This avoids resource contention during extension startup.
*/
private startBackendSyncAfterInitialAnalysis(): void {
try {
const backend = (this as any).backend;
if (backend && typeof backend.startTimerIfEnabled === 'function') {
backend.startTimerIfEnabled();
}
} catch (error) {
this.warn('Failed to start backend sync timer: ' + error);
}
}

public async updateTokenStats(silent: boolean = false): Promise<DetailedStats | undefined> {
try {
this.log('Updating token stats...');
Expand Down Expand Up @@ -6841,6 +6863,8 @@ private getMaturityHtml(webview: vscode.Webview, data: {

// Fetch all entities for the dataset using the facade's public API
const allEntities = await this.backend.getAggEntitiesForRange(settings, startKey, todayKey);

this.log(`[Dashboard] Fetched ${allEntities.length} entities for date range ${startKey} to ${todayKey}`);

// Aggregate personal data (all machines and workspaces for current user)
const personalDevices = new Set<string>();
Expand All @@ -6851,6 +6875,10 @@ private getMaturityHtml(webview: vscode.Webview, data: {

// Aggregate team data (all users)
const userMap = new Map<string, { tokens: number; interactions: number; cost: number }>();

// Track first and last data points for reference
let firstDate: string | null = null;
let lastDate: string | null = null;

for (const entity of allEntities) {
const userId = (entity.userId ?? '').toString();
Expand All @@ -6861,6 +6889,17 @@ private getMaturityHtml(webview: vscode.Webview, data: {
const outputTokens = Number.isFinite(Number(entity.outputTokens)) ? Number(entity.outputTokens) : 0;
const interactions = Number.isFinite(Number(entity.interactions)) ? Number(entity.interactions) : 0;
const tokens = inputTokens + outputTokens;
const dayKey = (entity.day ?? '').toString();

// Track date range
if (dayKey) {
if (!firstDate || dayKey < firstDate) {
firstDate = dayKey;
}
if (!lastDate || dayKey > lastDate) {
lastDate = dayKey;
}
}

// Personal data aggregation
if (userId === currentUserId) {
Expand Down Expand Up @@ -6917,6 +6956,8 @@ private getMaturityHtml(webview: vscode.Webview, data: {
const teamTotalInteractions = Array.from(userMap.values()).reduce((sum, u) => sum + u.interactions, 0);
const averageTokensPerUser = userMap.size > 0 ? teamTotalTokens / userMap.size : 0;

this.log(`[Dashboard] Date range: ${firstDate} to ${lastDate} (${teamMembers.length} team members)`);

return {
personal: {
userId: currentUserId,
Expand All @@ -6931,7 +6972,9 @@ private getMaturityHtml(webview: vscode.Webview, data: {
members: teamMembers,
totalTokens: teamTotalTokens,
totalInteractions: teamTotalInteractions,
averageTokensPerUser
averageTokensPerUser,
firstDate,
lastDate
},
lastUpdated: new Date().toISOString()
};
Expand Down Expand Up @@ -7472,6 +7515,7 @@ private getMaturityHtml(webview: vscode.Webview, data: {

// Get backend storage info
const backendStorageInfo = await this.getBackendStorageInfo();
this.log(`Backend storage info retrieved: enabled=${backendStorageInfo.enabled}, configured=${backendStorageInfo.isConfigured}`);

// Check if panel is still open before updating
if (!this.isPanelOpen(panel)) {
Expand All @@ -7480,6 +7524,7 @@ private getMaturityHtml(webview: vscode.Webview, data: {
}

// Send the loaded data to the webview
this.log(`Sending backend info to webview: ${backendStorageInfo ? 'present' : 'missing'}`);
panel.webview.postMessage({
command: 'diagnosticDataLoaded',
report,
Expand Down Expand Up @@ -7998,6 +8043,9 @@ export function activate(context: vscode.ExtensionContext) {
// Store backend facade in the tracker instance for dashboard access
(tokenTracker as any).backend = backendFacade;

// Backend sync timer will be started after initial token analysis completes
// (see startBackendSyncAfterInitialAnalysis method)

const configureBackendCommand = vscode.commands.registerCommand('copilot-token-tracker.configureBackend', async () => {
await backendHandler.handleConfigureBackend();
});
Expand Down
29 changes: 28 additions & 1 deletion src/webview/dashboard/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface DashboardStats {
totalTokens: number;
totalInteractions: number;
averageTokensPerUser: number;
firstDate?: string | null;
lastDate?: string | null;
};
lastUpdated: string | Date;
}
Expand Down Expand Up @@ -189,9 +191,33 @@ function buildTeamSection(stats: DashboardStats): HTMLElement {
buildStatCard('Avg per User', formatNumber(Math.round(stats.team.averageTokensPerUser)) + ' tokens')
);

// Add date range info if available
console.log('Team firstDate:', stats.team.firstDate, 'lastDate:', stats.team.lastDate);
let dateInfo: HTMLElement | null = null;
if (stats.team.firstDate || stats.team.lastDate) {
dateInfo = el('div', 'info-box');
dateInfo.style.cssText = 'margin-top: 16px; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 6px; font-size: 13px; color: #aaa;';
const firstDate = stats.team.firstDate;
const lastDate = stats.team.lastDate;
if (firstDate && lastDate) {
dateInfo.textContent = `📅 Data Range: ${firstDate} to ${lastDate}`;
} else if (firstDate) {
dateInfo.textContent = `📅 First Data: ${firstDate}`;
} else if (lastDate) {
dateInfo.textContent = `📅 Last Data: ${lastDate}`;
}
console.log('Date info element created');
} else {
console.log('No date range data available');
}

const leaderboard = buildLeaderboard(stats);

section.append(sectionTitle, teamGrid, leaderboard);
if (dateInfo) {
section.append(sectionTitle, teamGrid, dateInfo, leaderboard);
} else {
section.append(sectionTitle, teamGrid, leaderboard);
}
return section;
}

Expand Down Expand Up @@ -301,6 +327,7 @@ window.addEventListener('message', (event) => {
const message = event.data;
switch (message.command) {
case 'dashboardData':
console.log('Dashboard data received:', JSON.stringify(message.data.team, null, 2));
render(message.data);
break;
case 'dashboardLoading':
Expand Down
2 changes: 2 additions & 0 deletions src/webview/diagnostics/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@ function renderLayout(data: DiagnosticsData): void {
// Re-attach event listeners for backend buttons
setupBackendButtonHandlers();
}
} else {
console.warn("diagnosticDataLoaded received but backendStorageInfo is missing or undefined");
}

// Update session folders if provided
Expand Down
Loading