+
{!allowedHosts.length && (
No whitelisted domains
)}
@@ -101,7 +123,7 @@ export const WhitelistDomainSetting: React.FC = () => {
handleRemoveAllowedHost(domain)}
/>
{domain}
diff --git a/src/popup/hooks/PopupContext.tsx b/src/popup/hooks/PopupContext.tsx
index a9b2b96..4f3182f 100644
--- a/src/popup/hooks/PopupContext.tsx
+++ b/src/popup/hooks/PopupContext.tsx
@@ -69,7 +69,7 @@ export const PopupContextProvider: React.FC = ({ children }) => {
activeHostname: host || '',
settings,
store: filteredStore,
- updateSettings,
+ updateSettings
}}
>
{children}
diff --git a/src/popup/hooks/useTheme.ts b/src/popup/hooks/useTheme.ts
index 755b33d..0f7da33 100644
--- a/src/popup/hooks/useTheme.ts
+++ b/src/popup/hooks/useTheme.ts
@@ -1,6 +1,9 @@
type Themes = 'light' | 'dark' | 'auto';
-let theme: Themes = (localStorage.getItem('theme') as any) || 'light';
+const stored = localStorage.getItem('theme');
+let theme: Themes = stored === 'light' || stored === 'dark' || stored === 'auto'
+ ? stored
+ : 'light';
export const useTheme = () => {
return theme;
diff --git a/src/popup/hooks/useTimeStore.tsx b/src/popup/hooks/useTimeStore.tsx
index 28ea8bd..2584bac 100644
--- a/src/popup/hooks/useTimeStore.tsx
+++ b/src/popup/hooks/useTimeStore.tsx
@@ -16,9 +16,8 @@ export const useTimeStore = () => {
if (activeRecord?.hostname) {
const date = getIsoDate(new Date());
const currentDayActivity = (activity[date] ??= {});
- currentDayActivity[activeRecord.hostname] ??= 0;
- currentDayActivity[activeRecord.hostname] +=
- activeRecord.activityPeriodEnd - activeRecord.activityPeriodStart;
+ currentDayActivity[activeRecord?.hostname] =
+ (currentDayActivity[activeRecord?.hostname] ?? 0) + activeRecord.activityPeriodEnd - activeRecord.activityPeriodStart;
}
setStore(activity);
diff --git a/src/popup/pages/ActivityPage.tsx b/src/popup/pages/ActivityPage.tsx
index 0df9882..fbd2bec 100644
--- a/src/popup/pages/ActivityPage.tsx
+++ b/src/popup/pages/ActivityPage.tsx
@@ -12,6 +12,8 @@ import { DailyActivityTab } from '../components/ActivityPageDailyActivityTab/Act
import { ActivityPageWeeklyActivityTab } from '../components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab';
import { WeekDatePicker } from '../components/WeekDatePicker/WeekDatePicker';
import { usePopupContext } from '../hooks/PopupContext';
+import { MonthDatePicker } from '../components/MonthDatePicker/MonthDatePicker';
+import { ActivityPageMonthlyActivityTab } from '../components/ActivityPageMonthlyActivityTab/ActivityPageMonthlyActivityTab';
interface ActivityPageProps {
date?: string;
@@ -20,6 +22,7 @@ interface ActivityPageProps {
enum ActivityPageTabs {
Daily,
Weekly,
+ Monthly
}
export const ActivityPage: React.FC = ({
@@ -46,6 +49,7 @@ export const ActivityPage: React.FC = ({
buttonType={
activeTab === value ? ButtonType.Primary : ButtonType.Secondary
}
+ className='px-2'
onClick={() => setActiveTab(value)}
key={key}
>
@@ -71,6 +75,12 @@ export const ActivityPage: React.FC = ({
onWeekChange={setPickedSunday}
/>
)}
+ {activeTab === ActivityPageTabs.Monthly && (
+
+ )}
{activeTab === ActivityPageTabs.Daily && (
@@ -83,6 +93,12 @@ export const ActivityPage: React.FC = ({
sundayDate={pickedSunday}
/>
)}
+ {activeTab === ActivityPageTabs.Monthly && (
+
+ )}
>
);
};
diff --git a/src/popup/pages/OverallPage.tsx b/src/popup/pages/OverallPage.tsx
index 77bfb7f..1614aea 100644
--- a/src/popup/pages/OverallPage.tsx
+++ b/src/popup/pages/OverallPage.tsx
@@ -54,6 +54,7 @@ export const OverallPage: React.FC = ({
activityTimeline={timelineEvents}
filteredHostname={null}
emptyHoursMarginCount={0}
+ description="Your web activity in last 6 hours."
/>
);
diff --git a/src/popup/pages/PreferencesPage.tsx b/src/popup/pages/PreferencesPage.tsx
index 4633116..52a158b 100644
--- a/src/popup/pages/PreferencesPage.tsx
+++ b/src/popup/pages/PreferencesPage.tsx
@@ -4,10 +4,11 @@ import {FC} from 'react';
import { Panel, PanelBody, PanelHeader } from './../../blocks/Panel';
import { Input } from './../../blocks/Input';
-
import {IgnoredDomainSetting} from '../components/IgnoredDomainsSetting/IgnoredDomainSetting';
import { WhitelistDomainSetting } from '../components/WhitelistDomainsSetting/WhitelistDomainSetting';
import {UserTokenSetting} from "../components/UserTokenSetting/UserTokenSetting";
+import { ThemeSelector } from '../components/ThemeSelector';
+import { Logger } from '../components/Logger/Logger';
export const PreferencesPage: FC = () => {
const [isWhitelistShown, hideWhitelist] = React.useState
(true);
@@ -45,6 +46,8 @@ export const PreferencesPage: FC = () => {
{(isWhitelistShown ? : )}
+
+ {}
);
};
diff --git a/src/popup/selectors/get-total-monthly-activity.ts b/src/popup/selectors/get-total-monthly-activity.ts
new file mode 100644
index 0000000..62508a5
--- /dev/null
+++ b/src/popup/selectors/get-total-monthly-activity.ts
@@ -0,0 +1,10 @@
+import { get30DaysPriorDate } from '../../shared/utils/dates-helper';
+
+import { TimeStore } from '../hooks/useTimeStore';
+
+import { getTotalDailyActivity } from './get-total-daily-activity';
+
+export const getTotalMonthlyActivity = (store: TimeStore, date = new Date()) =>
+ get30DaysPriorDate(date).reduce((sum, date) => {
+ return sum + getTotalDailyActivity(store, date);
+ }, 0);
diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts
index e8e5e35..4ad4cf5 100644
--- a/src/shared/api/client.ts
+++ b/src/shared/api/client.ts
@@ -1,14 +1,20 @@
import {
+ ProfileResponse,
TokenProperties,
WebActivityLog,
WebActivityRecord,
} from '../db/types';
+
import {
CodealikeHost,
CurrentClientVersion,
InvalidTokenError,
} from './constants';
+import { Logger } from '../utils/logger';
+
+const SOURCE = 'SHARED/API/CLIENT';
+
const getHeaders = (userId: string, uuid: string): Record
=> {
return {
'Content-Type': 'application/json;charset=utf-8',
@@ -22,37 +28,31 @@ export const sendStats = async (
token: string,
records: WebActivityRecord[],
states: WebActivityLog[],
-): Promise<{ result: boolean }> => {
- return new Promise((resolve, reject) => {
- try {
- const { userId, uuid } = getTokenProperties(token);
- const url = `${CodealikeHost}/webactivity/SaveWebActivity`;
+): Promise => {
+ try {
+ const { userId, uuid } = getTokenProperties(token);
+ const url = `${CodealikeHost}/webactivity/SaveWebActivity`;
- fetch(url, {
- body: JSON.stringify({
- Extension: CurrentClientVersion,
- WebActivity: records,
- WebActivityLog: states,
- }),
- headers: getHeaders(userId, uuid),
- method: 'POST',
- })
- .then((result) => {
- if (result.status === 200) {
- resolve({ result: true });
- } else {
- reject();
- }
- })
- .catch((err) => {
- console.log(err);
- reject();
- });
- } catch (err) {
- console.log((err as Error).message);
- reject();
+ const response = await fetch(url, {
+ body: JSON.stringify({
+ Extension: CurrentClientVersion,
+ WebActivity: records,
+ WebActivityLog: states,
+ }),
+ headers: getHeaders(userId, uuid),
+ method: 'POST',
+ });
+
+ if (response.status === 200) {
+ return true;
+ } else {
+ throw new Error(`Request failed with status ${response.status}`);
}
- });
+ } catch (err) {
+ const errorObj = err instanceof Error ? err : new Error(String(err));
+ Logger.error(SOURCE,`sendStats:`, errorObj)
+ return false;
+ }
};
export const authorize = (token: string): Promise<{ result: boolean }> => {
@@ -82,6 +82,34 @@ export const authorize = (token: string): Promise<{ result: boolean }> => {
});
};
+export const getProfile = (token: string): Promise => {
+ return new Promise((resolve, reject) => {
+ const { userId, uuid } = getTokenProperties(token);
+ const url = `${CodealikeHost}/account/${userId}/profile`;
+ console.log(`url: ${url}`)
+
+ fetch(url, {
+ headers: getHeaders(userId, uuid),
+ method: 'GET',
+ })
+ .then((result) => {
+ if (result.status === 200) {
+ return result.json()
+ } else {
+ reject();
+ }
+ })
+ .then((response) => {
+ console.log('Response body:', response);
+ resolve(response);
+ })
+ .catch((err) => {
+ console.log((err as Error).message);
+ reject();
+ });
+ });
+};
+
const getTokenProperties = (token: string): TokenProperties => {
if (token === undefined) {
throw new Error(InvalidTokenError);
diff --git a/src/shared/api/constants.ts b/src/shared/api/constants.ts
index 23f9647..acd3f6f 100644
--- a/src/shared/api/constants.ts
+++ b/src/shared/api/constants.ts
@@ -1,7 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../../../package.json');
+export const IS_PRODUCTION_ENVIRONMENT = process.env.NODE_ENV === 'production'
-export const CodealikeHost = 'https://codealike.com/api/v2';
+export const CodealikeHost = (IS_PRODUCTION_ENVIRONMENT ? 'https://codealike.com/api/v2' : 'https://dev.codealike.com/api/v2');
export const InvalidTokenError = 'Invalid token provided';
export const CurrentClientVersion = packageJson.version;
diff --git a/src/shared/db/idb.ts b/src/shared/db/idb.ts
index 9faa40e..3cc5de9 100644
--- a/src/shared/db/idb.ts
+++ b/src/shared/db/idb.ts
@@ -1,4 +1,4 @@
-import { DBSchema, openDB } from 'idb';
+import { DBSchema, openDB, IDBPDatabase } from 'idb';
import { ActiveTabState, LogMessage, TimeStore, TimelineRecord } from './types';
@@ -7,6 +7,7 @@ export enum Database {
}
export enum TimeTrackerStoreTables {
+ Id = 'id',
Timeline = 'timeline',
State = 'state',
Logs = 'logs',
@@ -40,8 +41,18 @@ export interface TimelineDatabase extends DBSchema {
};
}
-export const connect = () =>
- openDB(Database.TimeTrackerStore, DB_VERSION, {
+// --- Internal Global Connection Variable ---
+let _db: IDBPDatabase | null = null;
+
+export const connect = async (): Promise> => {
+ if (_db) {
+ // If a connection already exists, return it
+ //Logger.debug(`Already Opened IndexedDB: ${Database.TimeTrackerStore} (Version: ${DB_VERSION})`);
+ return _db;
+ }
+
+ // Logger.debug(`DB::connect : Opening IndexedDB:`);
+ _db = await openDB(Database.TimeTrackerStore, DB_VERSION, {
upgrade(db, oldVersion, newVersion, transaction) {
if (oldVersion < 1) {
const tabsStateStore = db.createObjectStore(
@@ -61,12 +72,10 @@ export const connect = () =>
const timelineStore = db.createObjectStore(
TimeTrackerStoreTables.Timeline,
{
-
// If it isn't explicitly set, create a value by auto incrementing.
-autoIncrement: true,
-
- // The 'id' property of the object will be the key.
-keyPath: 'id',
+ autoIncrement: true,
+ // The 'id' property of the object will be the key.
+ keyPath: 'id',
}
);
@@ -91,3 +100,15 @@ keyPath: 'id',
}
},
});
+ return _db;
+}
+
+export const disconnect = async (): Promise => {
+ if (_db) {
+ _db.close();
+ _db = null;
+ // Logger.info("IndexedDB connection disconnected.");
+ } else {
+ // Logger.debug("DB::No active IndexedDB connection to disconnect.");
+ }
+};
\ No newline at end of file
diff --git a/src/shared/db/sync-storage.ts b/src/shared/db/sync-storage.ts
index db02dac..2be1952 100644
--- a/src/shared/db/sync-storage.ts
+++ b/src/shared/db/sync-storage.ts
@@ -1,22 +1,52 @@
-import { getIsoDate } from '../utils/dates-helper';
-import { mergeTimeStore } from '../utils/merge-time-store';
+import { getIsoDate, getTimeFromMs } from '../utils/dates-helper';
import {
connect,
TimeTrackerStoreStateTableKeys,
- TimeTrackerStoreTables,
+ TimeTrackerStoreTables
} from './idb';
+import { getSettings } from '../../shared/preferences';
import { TimeStore } from './types';
-const getDbCache = async (): Promise => {
+import {
+ Preferences,
+ ConnectionStatus
+} from '../../shared/db/types';
+
+import { Logger } from '../../shared/utils/logger';
+
+const SOURCE = 'DB/SYNC-Stroage';
+
+export const getDbCache = async (): Promise => {
const db = await connect();
const store = await db.get(
TimeTrackerStoreTables.State,
TimeTrackerStoreStateTableKeys.OverallState,
);
-
return (store || {}) as TimeStore;
};
-const setDbCache = async (store: TimeStore) => {
+
+
+type LogsHelper = {
+ [date: string]: {
+ [domain: string]: {
+ localTime: string;
+ timeSpentMinutes: string ;
+ timeSpentMs: number | undefined;
+ };
+ };
+};
+
+
+function getLocalTimeString(): string {
+ return new Date().toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ hour12: true,
+ minute: "2-digit",
+ second: "2-digit"
+ });
+}
+
+export const setDbCacheTimeStore = async (store: TimeStore) => {
const db = await connect();
await db.put(
TimeTrackerStoreTables.State,
@@ -26,40 +56,71 @@ const setDbCache = async (store: TimeStore) => {
};
const setTotalActivity = async (store: TimeStore) => {
- await setDbCache(store);
- await chrome.storage.local.set({
- activity: store,
- });
+ //Logger.debug(SOURCE,`setTotalActivity`, store);
+ await setDbCacheTimeStore(store);
};
+export const getLocalActivity = async (): Promise => {
+ const {activity={}} = await chrome.storage.local.get('activity')
+ return activity;
+}
+
export const getTotalActivity = async (): Promise => {
- const [localStore, dbStore] = await Promise.all([
- chrome.storage.local.get('activity').then((store) => store?.activity ?? {}),
- getDbCache(),
- ]);
- return mergeTimeStore(dbStore, localStore);
+ const preferences: Preferences = await getSettings();
+
+ // Check if account is not connected with API key
+ if (preferences.connectionStatus !== ConnectionStatus.Connected) {
+ const dbStore = await getDbCache();
+ return dbStore;
+ }
+
+ const dbStore = await getLocalActivity();
+ return dbStore
};
export const getCurrentHostTime = async (host: string): Promise => {
- const store = await getTotalActivity();
+ const store: TimeStore = await getTotalActivity();
const currentDate = getIsoDate(new Date());
- return (store[currentDate] as any)?.[host] ?? 0;
+ return store[currentDate]?.[host] ?? 0;
};
export const setTotalDailyHostTime = async ({
date: day,
host,
- duration,
+ duration
}: {
date: string;
host: string;
duration: number;
}) => {
- const store = await getTotalActivity();
+ const store:TimeStore = await getDbCache();
+ const dayActivity = (store[day] ??= {}) as Record;
+ const existingDuration = (dayActivity[host] ?? 0)
- const dayActivity = (store[day] ??= {});
dayActivity[host] = duration;
- return setTotalActivity(store);
+ const logStr = "\n--- setTotal Time Cache --\nHost: " + host +
+ "\nExisting Duration: " + getTimeFromMs(existingDuration) + " - "+ existingDuration+ "ms" +
+ "\nNEW Duration : " + getTimeFromMs(duration) + " - " + duration + "ms";
+
+ const logsHelperObj: LogsHelper = {};
+if (dayActivity && Object.keys(dayActivity).length > 0) {
+ if (!logsHelperObj[day]) {
+ logsHelperObj[day] = {};
+ }
+
+ for (const domain in dayActivity) {
+ const time = dayActivity[domain] || 0;
+ (logsHelperObj[day] ??= {})[domain] = {
+ localTime: getLocalTimeString(),
+ timeSpentMinutes: getTimeFromMs(time),
+ timeSpentMs: time,
+ };
+ }
+ }
+ Logger.debug(SOURCE, `setTotalDailyHostTime: ${logStr}`, (logsHelperObj??{}));
+
+
+ await setTotalActivity(store);
};
diff --git a/src/shared/db/types.ts b/src/shared/db/types.ts
index e31518c..19e537b 100644
--- a/src/shared/db/types.ts
+++ b/src/shared/db/types.ts
@@ -22,6 +22,7 @@ export interface WebActivityRecord {
export type TimelineRecordStatus = 'navigation' | 'debugging' | 'debugger';
export interface TimelineRecord {
+ id?: number ;
tabId: number;
url: string;
hostname: string;
@@ -32,6 +33,7 @@ export interface TimelineRecord {
secure: boolean;
activityPeriodStart: number;
activityPeriodEnd: number;
+ synced?:boolean;
}
export type ActiveTabState = {
@@ -60,7 +62,9 @@ export interface Preferences {
allowedHosts?: string[]; //urls that are to be whitelisted
limits: Record;
displayTimeOnBadge: boolean;
+ enableLogging: boolean;
lastUpdateStats?: Statistics;
+ username?: string
}
export interface Statistics {
@@ -78,3 +82,28 @@ export interface TokenProperties {
userId: string;
uuid: string;
}
+
+export interface ProfileResponse {
+ Identity: string
+ FullName: string
+ DisplayName: string
+ Address?: string
+ State?: string
+ Country?: string
+ AvatarUri: string
+ Email: string
+}
+
+export interface RecordWithKey {
+ key: IDBValidKey;
+ value: T;
+}
+
+export interface LogEntry {
+ timestamp: string;
+ level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
+ source: string;
+ message: string;
+ context ? : object | undefined;
+}
+
diff --git a/src/shared/preferences/index.ts b/src/shared/preferences/index.ts
index 62096a3..74b82f8 100644
--- a/src/shared/preferences/index.ts
+++ b/src/shared/preferences/index.ts
@@ -4,6 +4,7 @@ export const DEFAULT_PREFERENCES: Preferences = {
allowedHosts: [], //whitelisted domains
connectionStatus: ConnectionStatus.Disconnected,
displayTimeOnBadge: true,
+ enableLogging: false,
ignoredHosts: [],
limits: {},
};
@@ -16,9 +17,15 @@ export const setSettings = async (settings: Partial) => {
};
export const getSettings = async () => {
- const { settings = {} } = await chrome.storage.local.get('settings');
- return {
- ...DEFAULT_PREFERENCES,
- ...settings,
- } as Preferences;
+
+ if(!(chrome && chrome?.storage && chrome?.storage.local)){
+ console.log("chrome object not existing");
+ return {...DEFAULT_PREFERENCES};
+ }
+
+ const { settings = {} } = await chrome.storage.local.get('settings');
+ return {
+ ...DEFAULT_PREFERENCES,
+ ...settings,
+ } as Preferences;
};
diff --git a/src/shared/utils/dates-helper.ts b/src/shared/utils/dates-helper.ts
index 72759ad..2012ca7 100644
--- a/src/shared/utils/dates-helper.ts
+++ b/src/shared/utils/dates-helper.ts
@@ -45,20 +45,36 @@ export const getTimeWithoutSeconds = (number: number) => {
.join(' ');
};
-export const get7DaysPriorDate = <
- T extends (date: Date) => any = (date: Date) => Date
->(
+export const get7DaysPriorDate = (
date: Date,
- map?: T
-): ReturnType[] => {
- const defaultMap = (date: Date) => new Date(date);
- const weekEndDate = new Date(date);
+ map?: (date: Date) => Date
+): Date[] => {
+ const defaultMap = (d: Date) => new Date(d);
+ const results: Date[] = [];
+
+ for (let i = 0; i < 7; i++) {
+ const d = new Date(date);
+ d.setDate(d.getDate() - i);
+ results.push((map ?? defaultMap)(d));
+ }
+
+ return results;
+};
- return new Array(7).fill(0).map((_, index) => {
- weekEndDate.setDate(weekEndDate.getDate() - Number(index > 0));
+export const get30DaysPriorDate = (
+ date: Date,
+ map?: (date: Date) => Date
+): Date[] => {
+ const defaultMap = (d: Date) => new Date(d);
+ const results: Date[] = [];
+
+ for (let i = 0; i < 30; i++) {
+ const d = new Date(date);
+ d.setDate(d.getDate() - i);
+ results.push((map ?? defaultMap)(d));
+ }
- return map?.(weekEndDate) ?? defaultMap(weekEndDate);
- });
+ return results;
};
export const getDatesWeekSundayDate = (date: Date = new Date()) => {
diff --git a/src/shared/utils/logger.ts b/src/shared/utils/logger.ts
new file mode 100644
index 0000000..4ddb79f
--- /dev/null
+++ b/src/shared/utils/logger.ts
@@ -0,0 +1,146 @@
+import { Preferences,LogEntry } from '../../shared/db/types';
+import { getSettings } from '../../shared/preferences';
+
+const LOG_STORAGE_KEY = 'codealike_extension_logs';
+const MAX_LOG_ENTRIES = 1000;
+
+// Defines the structure of a single log entry.
+
+interface LoggerAPI {
+ info(source: string, message: string, context ? : object): Promise < void > ;
+ warn(source: string, message: string, context ? : object): Promise < void > ;
+ error(source: string, message: string, context ? : object): Promise < void > ;
+ debug(source: string, message: string, context ? : object): Promise < void > ;
+ get(): Promise < LogEntry[] > ;
+ clear(): Promise < void > ;
+}
+
+// In-memory cache for preferences
+let _cachedPreferences: Preferences | null = null;
+let _cacheTimestamp: number | null = null;
+const CACHE_EXPIRATION_MS = 5 * 1000; // 5 seconds in milliseconds
+
+ async function _getPreferences(): Promise {
+ const now = Date.now();
+
+ // Check if cache exists and is not expired
+ if (_cachedPreferences && _cacheTimestamp && (now - _cacheTimestamp < CACHE_EXPIRATION_MS)) {
+ console.log("Returning settings from cache.");
+ return _cachedPreferences;
+ }
+
+ console.log("Cache expired or not present, fetching settings from storage.");
+ try {
+ const fetchedPreferences = await getSettings();
+ _cachedPreferences = fetchedPreferences;
+ _cacheTimestamp = now; // Update timestamp when new data is fetched
+ return fetchedPreferences;
+ } catch (error) {
+ console.log("Failed to fetch preferences from storage. Returning potentially stale cache or defaults.", error);
+ // In case of error, return cached preferences if they exist, even if expired
+ // This provides a fallback for resilience, but cacheTimestamp is still updated.
+ if (_cachedPreferences) {
+ return _cachedPreferences;
+ }
+ // If no cache and error, getSettingsFromStorage handles returning defaults.
+ throw error; // Re-throw if getSettingsFromStorage propagates error
+ }
+}
+
+/**
+ * @param level - The log level ('INFO', 'WARN', 'ERROR', 'DEBUG').
+ * @param message - The log message.
+ * @param context - Optional context object to store with the log.
+ */
+async function addLog(level: LogEntry['level'], source: string, message: string, context ? : object ): Promise < void > {
+ const preferences: Preferences = await getSettings();
+ if(preferences.enableLogging!==true){
+ return ;
+ }
+
+ const timestamp = new Date().toISOString();
+ const logEntry: LogEntry = {
+ context,
+ level,
+ message,
+ source,
+ timestamp,
+ };
+
+ try {
+ const result = await chrome.storage.local.get(LOG_STORAGE_KEY);
+ let logs: LogEntry[] = result[LOG_STORAGE_KEY] || [];
+
+ logs.push(logEntry);
+
+ if (logs.length > MAX_LOG_ENTRIES) {
+ logs = logs.slice(logs.length - MAX_LOG_ENTRIES);
+ }
+
+ await chrome.storage.local.set({
+ [LOG_STORAGE_KEY]: logs
+ });
+ const contextStr = context !== undefined && context !== null
+ ? `\nData:\n${typeof context === 'object' ? JSON.stringify(context, null, 2) : String(context)}` : '';
+ console.log(`${timestamp} [${level}] ${source} ${message} ${contextStr}`); // console.log for immediate visibility
+
+ } catch (error) {
+ console.error("Error adding log to storage:", error);
+ }
+}
+
+// Retrieves all stored logs from chrome.storage.local.
+async function getLogs(): Promise < LogEntry[] > {
+ try {
+ const result = await chrome.storage.local.get(LOG_STORAGE_KEY);
+ return result[LOG_STORAGE_KEY] || [];
+ } catch (error) {
+ console.error("Error retrieving logs from storage:", error);
+ return [];
+ }
+}
+
+// Clears all stored logs from chrome.storage.local.
+async function clearLogs(): Promise < void > {
+ try {
+ const result1 = await chrome.storage.local.get(LOG_STORAGE_KEY);
+ console.log("Before clear logs ", result1)
+ await chrome.storage.local.set({
+ [LOG_STORAGE_KEY]: []
+ });
+ await chrome.storage.local.remove(LOG_STORAGE_KEY);
+ console.log("Logs cleared from storage.");
+ } catch (error) {
+ console.error("Error clearing logs from storage:", error);
+ }
+}
+
+
+
+export const Logger: LoggerAPI = {
+ clear: clearLogs,
+ debug: (source, message, context) => addLog('DEBUG', source, message, context),
+ error: (source, message, context) => addLog('ERROR', source, message, context),
+ get: getLogs,
+ info: (source, message, context) => addLog('INFO', source, message, context),
+ warn: (source, message, context) => addLog('WARN', source, message, context),
+};
+
+// For environments where `Logger` needs to be globally accessible (e.g., background service worker)
+declare global {
+ interface Window {
+ Logger: LoggerAPI;
+ }
+ let Logger: LoggerAPI;
+}
+
+if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getURL('').startsWith('chrome-extension://')) {
+ // This check helps ensure we're in a Chrome extension environment
+ if (typeof self !== 'undefined' && self.ServiceWorker) {
+ // This is a service worker (Manifest V3 background script)
+ self.Logger = Logger;
+ } else if (typeof window !== 'undefined') {
+ // This is a traditional background page (Manifest V2)
+ window.Logger = Logger;
+ }
+}
\ No newline at end of file
diff --git a/src/shared/utils/merge-time-store.ts b/src/shared/utils/merge-time-store.ts
index 3b06e32..698599a 100644
--- a/src/shared/utils/merge-time-store.ts
+++ b/src/shared/utils/merge-time-store.ts
@@ -16,16 +16,62 @@ export const mergeTimeStore = (
acc[key] = {
...storeAValue,
...storeBValue,
- ...Object.keys({ ...storeAValue, ...storeBValue }).reduce((acc, key) => {
- const storeAValueForKey = storeAValue?.[key] || storeBValue?.[key] || 0;
- const storeBValueForKey = storeBValue?.[key] || storeAValue?.[key] || 0;
+ ...Object.keys({ ...storeAValue, ...storeBValue }).reduce((nestedAcc, innerKey) => {
+ const storeAValueForKey = storeAValue?.[innerKey] || storeBValue?.[innerKey] || 0;
+ const storeBValueForKey = storeBValue?.[innerKey] || storeAValue?.[innerKey] || 0;
- acc[key] = Math.max(storeAValueForKey, storeBValueForKey);
+ nestedAcc[innerKey] = Math.max(storeAValueForKey, storeBValueForKey);
- return acc;
+ return nestedAcc;
}, {} as Record),
};
return acc;
}, {} as TimeStore);
};
+
+function getAllKeys(objA: T, objB: T): string[] {
+ const keysA = Object.keys(objA || {});
+ const keysB = Object.keys(objB || {});
+ return Array.from(new Set([...keysA, ...keysB]));
+}
+
+function sumSubKeys(
+ subA: Record = {},
+ subB: Record = {}
+): Record {
+ // Ensure both are objects, not null
+ subA=subA || {};
+ subB= subB || {}
+ const allSubKeys = getAllKeys(subA, subB);
+ const subResult: Record = {};
+
+ for (const subKey of allSubKeys) {
+ const aVal = Number(subA[subKey]) || 0;
+ const bVal = Number(subB[subKey]) || 0;
+ const sum = aVal + bVal;
+ subResult[subKey] = Number.isFinite(sum) ? sum : 0;
+ }
+
+ return subResult;
+}
+
+export function sumTimeStores(
+ storeA: TimeStore = {},
+ storeB: TimeStore = {}
+): TimeStore {
+ // Ensure both are objects, not null
+ storeA=storeA || {};
+ storeB= storeB || {}
+ const allKeys = getAllKeys(storeA, storeB);
+ const result: TimeStore = {};
+
+ for (const key of allKeys) {
+ const subResult = sumSubKeys(storeA[key] ?? {}, storeB[key] ?? {});
+ if (Object.keys(subResult).length > 0) {
+ result[key] = subResult;
+ }
+ }
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/shared/utils/systemInfo.ts b/src/shared/utils/systemInfo.ts
new file mode 100644
index 0000000..0fbed87
--- /dev/null
+++ b/src/shared/utils/systemInfo.ts
@@ -0,0 +1,199 @@
+export interface SystemSummary {
+ availableMemoryGB: string;
+ chromeVersion: string;
+ cpuArchitecture: string;
+ cpuCores: number;
+ cpuType: string;
+ extensionVersion: string;
+ language: string;
+ osType: string;
+ osVersion: string;
+ timestamp: string;
+ totalMemoryGB: string;
+ usedMemoryGB: string;
+ userAgent: string;
+}
+
+/**
+ * Fallback to get OS info from userAgent string parsing.
+ * Handles Windows, macOS, Linux.
+ */
+function getOsInfoFromLegacyNavigator(): Partial {
+ const osInfo: Partial = {
+ cpuArchitecture: "N/A",
+ osType: "N/A",
+ osVersion: "N/A",
+ };
+
+ const userAgent = navigator.userAgent || "";
+ const platform = navigator.platform || "";
+
+ if (/Windows NT (\d+\.\d+)/.test(userAgent)) {
+ osInfo.osType = "Windows";
+ osInfo.osVersion = userAgent.match(/Windows NT (\d+\.\d+)/)?.[1] ?? "N/A";
+ } else if (/Mac OS X (\d+(?:[_.]\d+)+)/.test(userAgent)) {
+ osInfo.osType = "macOS";
+ osInfo.osVersion =
+ userAgent.match(/Mac OS X (\d+(?:[_.]\d+)+)/)?.[1]?.replace(/_/g, ".") ?? "N/A";
+ } else if (/Linux/.test(platform) || /Linux/.test(userAgent)) {
+ osInfo.osType = "Linux";
+ osInfo.osVersion = "N/A";
+ } else {
+ osInfo.osType = platform || "Unknown";
+ }
+
+ osInfo.cpuArchitecture = "N/A"; // No reliable fallback for architecture here
+
+ return osInfo;
+}
+
+interface UserAgentData {
+ getHighEntropyValues(
+ hints: string[]
+ ): Promise<{ platformVersion?: string; architecture?: string; platform?: string }>;
+ platform?: string;
+ brands?: Array<{ brand: string; version: string }>;
+}
+
+interface NavigatorWithUAData extends Navigator {
+ userAgentData?: UserAgentData;
+}
+
+async function getOsInfo(): Promise> {
+ const nav = navigator as NavigatorWithUAData;
+
+ if (nav.userAgentData) {
+ try {
+ const uaData = await nav.userAgentData.getHighEntropyValues([
+ "platformVersion",
+ "architecture",
+ "platform",
+ ]);
+ return {
+ cpuArchitecture: uaData.architecture ?? "N/A",
+ osType: uaData.platform ?? "N/A",
+ osVersion: uaData.platformVersion ?? "N/A",
+ };
+ } catch (e) {
+ console.warn("Failed to get high-entropy user agent data, falling back:", e);
+ return getOsInfoFromLegacyNavigator();
+ }
+ } else {
+ return getOsInfoFromLegacyNavigator();
+ }
+}
+
+async function getCpuDetails(): Promise> {
+ const cpuDetails: Partial = {
+ cpuArchitecture: "N/A",
+ cpuCores: 0,
+ cpuType: "N/A",
+ };
+ try {
+ if (chrome?.system?.cpu?.getInfo) {
+ const cpuInfo = await chrome.system.cpu.getInfo();
+ cpuDetails.cpuArchitecture = cpuInfo.archName || "N/A";
+ cpuDetails.cpuCores = cpuInfo.numOfProcessors || 0;
+ cpuDetails.cpuType = cpuInfo.modelName || "N/A";
+ }
+ } catch (e) {
+ console.warn("Could not get CPU info:", e);
+ }
+ return cpuDetails;
+}
+
+async function getMemoryDetails(): Promise> {
+ const memDetails: Partial = {
+ availableMemoryGB: "N/A",
+ totalMemoryGB: "N/A",
+ usedMemoryGB: "N/A",
+ };
+ try {
+ if (chrome?.system?.memory?.getInfo) {
+ const memoryInfo = await chrome.system.memory.getInfo();
+ const totalGB = (memoryInfo.capacity / (1024 ** 3)).toFixed(2);
+ const availableGB = (memoryInfo.availableCapacity / (1024 ** 3)).toFixed(2);
+
+ memDetails.availableMemoryGB = `${availableGB} GB`;
+ memDetails.totalMemoryGB = `${totalGB} GB`;
+
+ const total = parseFloat(totalGB);
+ const available = parseFloat(availableGB);
+ memDetails.usedMemoryGB = !isNaN(total) && !isNaN(available)
+ ? `${(total - available).toFixed(2)} GB`
+ : "N/A";
+ }
+ } catch (e) {
+ console.warn("Could not get Memory info:", e);
+ }
+ return memDetails;
+}
+
+function getBaseBrowserInfo(): Partial {
+ return {
+ chromeVersion: navigator.userAgent.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/)?.[1] || "N/A",
+ extensionVersion: chrome.runtime.getManifest()?.version || "N/A",
+ language: navigator.language || "N/A",
+ timestamp: new Date().toISOString(),
+ userAgent: navigator.userAgent || "N/A",
+ };
+}
+
+function getOrDefault(...values: (T | undefined)[]): T {
+ for (const v of values) {
+ if (v !== undefined && v !== null) return v;
+ }
+ throw new Error("No value provided.");
+}
+
+function finalizeSystemSummary(
+ baseInfo: Partial,
+ osInfo: Partial,
+ cpuDetails: Partial,
+ memoryDetails: Partial
+): SystemSummary {
+ return {
+ availableMemoryGB: getOrDefault(memoryDetails.availableMemoryGB, "N/A"),
+ chromeVersion: getOrDefault(baseInfo.chromeVersion, "N/A"),
+ cpuArchitecture: getOrDefault(osInfo.cpuArchitecture, cpuDetails.cpuArchitecture, "N/A"),
+ cpuCores: getOrDefault(cpuDetails.cpuCores, 0),
+ cpuType: getOrDefault(cpuDetails.cpuType, "N/A"),
+ extensionVersion: getOrDefault(baseInfo.extensionVersion, "N/A"),
+ language: getOrDefault(baseInfo.language, "N/A"),
+ osType: getOrDefault(osInfo.osType, "N/A"),
+ osVersion: getOrDefault(osInfo.osVersion, "N/A"),
+ timestamp: getOrDefault(baseInfo.timestamp, new Date().toISOString()),
+ totalMemoryGB: getOrDefault(memoryDetails.totalMemoryGB, "N/A"),
+ usedMemoryGB: getOrDefault(memoryDetails.usedMemoryGB, "N/A"),
+ userAgent: getOrDefault(baseInfo.userAgent, "N/A"),
+ };
+}
+
+export async function getSystemSummary(): Promise {
+ const [baseSummary, osInfo, cpuDetails, memoryDetails] = await Promise.all([
+ Promise.resolve(getBaseBrowserInfo()), // already synchronous
+ getOsInfo(),
+ getCpuDetails(),
+ getMemoryDetails(),
+ ]);
+
+ return finalizeSystemSummary(baseSummary, osInfo, cpuDetails, memoryDetails);
+}
+
+export function formatSystemSummary(summary: SystemSummary): string {
+ return `--- System Information ---
+Available Memory: ${summary.availableMemoryGB}
+Chrome Version: ${summary.chromeVersion}
+CPU Architecture: ${summary.cpuArchitecture}
+CPU Cores: ${summary.cpuCores}
+CPU Type: ${summary.cpuType}
+Extension Version: ${summary.extensionVersion}
+Language: ${summary.language}
+OS Type: ${summary.osType}
+OS Version: ${summary.osVersion}
+Timestamp: ${summary.timestamp}
+Total Memory: ${summary.totalMemoryGB}
+Used Memory: ${summary.usedMemoryGB}
+User Agent: ${summary.userAgent}
+--- End System Information ---\n\n`;
+}
\ No newline at end of file
diff --git a/static/.DS_Store b/static/.DS_Store
deleted file mode 100644
index 169ba7c..0000000
Binary files a/static/.DS_Store and /dev/null differ
diff --git a/static/manifest.json b/static/manifest.json
index 8d7a2d9..2dfb9f0 100644
--- a/static/manifest.json
+++ b/static/manifest.json
@@ -20,11 +20,14 @@
}
],
"minimum_chrome_version": "90",
- "version": "2.1.0",
+ "version": "2.2.0",
"description": "Track activity while coding.",
"permissions": [
"idle",
"storage",
+ "downloads",
+ "system.cpu",
+ "system.memory",
"tabs",
"activeTab",
"alarms",