# Chrome Extension — Developer Guide

**Version:** 1.1  
**Date:** June 9, 2026  
**Type:** Google Chrome extension (Manifest V3)  
**Purpose:** Unified workplace companion — SSO session, attendance, tasks, meetings, reviews, activity screenshots  
**Related:** [../backend/node.md](../backend/node.md), [../frondend/angular.md](../frondend/angular.md), [../docs/USE_CASES.md](../docs/USE_CASES.md), [review](../../review) app

---

## 1. Overview

The **Gventure Work Companion** Chrome extension is the **single desktop entry point** for employees across the Gventure stack:

| App | Role | Extension integration |
|-----|------|------------------------|
| **Leaves** | Leave, attendance, shifts | Day login/logout, balances, shift reminders |
| **Review** | L10 tasks, meetings, peer review | Task % progress, meeting alerts, review notifications |
| **Salary-slip** | Payslips, employee OTP auth | Shared JWT / OTP login source |

### Core capabilities

1. **Unified session (SSO)** — Log in once (OTP); extension stores JWT and reuses it for **all** connected apps without re-authenticating in each portal.
2. **Attendance** — Reminders + one-click **day login** / **day logout** to the leaves API.
3. **Task updates** — Poll review L10 todos; show **% completed** (weekly tasks) and notify on progress changes.
4. **Meeting notifications** — Remind before L10 weekly/monthly meetings (`meeting_day_of_week`, `meeting_time` from `l10_teams`).
5. **Review notifications** — Peer review pending, self-review due, assigned reviewer tasks.
6. **Random activity screenshots** — During an active attendance day, capture the visible tab on a **random schedule** and upload for **productivity / activity reports** (HR compliance).

The extension complements Angular SPAs; complex flows (shift editing, L10 prep, peer rating forms) deep-link into the correct portal.

### Architecture at a glance

```
┌──────────────────────────────────────────────────────────────────┐
│  Gventure Work Companion (MV3)                                    │
│  ┌──────────┐  ┌─────────────────┐  ┌─────────────────────────┐ │
│  │ Popup    │  │ Service Worker  │  │ Content Scripts (×3)    │ │
│  │ Dashboard│  │ SessionManager  │  │ leaves · review · slip  │ │
│  └────┬─────┘  │ TaskSync        │  │ → postMessage AUTH      │ │
│       │        │ ScreenshotSvc   │  └───────────┬─────────────┘ │
│       │        │ NotificationSvc │              │               │
│       └────────┴────────┬────────┴──────────────┘               │
└─────────────────────────┼────────────────────────────────────────┘
                          │ HTTPS + shared JWT
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
   Leaves API       Review API      Salary-slip API
   :3000            :4015           :3000 (shared)
          │               │
          └─────── MySQL salaryslip (shared employees, l10_teams)
```

---

## 2. Repository layout

```
chrome-app/
├── manifest.json
├── package.json
├── tsconfig.json
├── src/
│   ├── background/
│   │   ├── service-worker.ts
│   │   ├── SessionManager.ts           # Singleton: unified JWT + refresh
│   │   ├── AlarmScheduler.ts
│   │   ├── NotificationService.ts
│   │   ├── AttendanceSyncService.ts    # Day login / logout
│   │   ├── TaskProgressSyncService.ts  # Review L10 todos → % complete
│   │   ├── MeetingReminderService.ts   # L10 meeting alarms
│   │   ├── ReviewAlertService.ts       # Peer + self-review pending
│   │   └── ScreenshotCaptureService.ts # Random capture → report upload
│   ├── popup/
│   │   ├── popup.html
│   │   ├── popup.ts
│   │   └── popup.css
│   ├── options/
│   │   ├── options.html
│   │   └── options.ts
│   ├── content/
│   │   ├── auth-bridge.ts              # Shared bridge logic
│   │   ├── leaves-bridge.ts            # localhost:4200 (leaves portal)
│   │   ├── review-bridge.ts            # review SPA origin
│   │   └── salary-slip-bridge.ts       # salary-slip SPA origin
│   ├── shared/
│   │   ├── api/
│   │   │   ├── LeavesApiClient.ts
│   │   │   ├── ReviewApiClient.ts
│   │   │   └── ActivityReportApiClient.ts
│   │   ├── auth/
│   │   │   └── UnifiedTokenStore.ts    # chrome.storage.local
│   │   ├── config/
│   │   │   └── ConfigSingleton.ts
│   │   └── types/
│   └── assets/icons/
├── dist/
└── README.md
```

---

## 3. Manifest V3 (`manifest.json`)

```json
{
  "manifest_version": 3,
  "name": "Gventure Work Companion",
  "version": "1.1.0",
  "description": "Unified login, attendance, tasks, meetings, reviews, and activity reporting",
  "permissions": [
    "alarms",
    "notifications",
    "storage",
    "tabs",
    "activeTab"
  ],
  "host_permissions": [
    "http://localhost:3000/*",
    "http://localhost:4015/*",
    "http://localhost:4200/*",
    "http://localhost:4201/*",
    "https://YOUR_LEAVES_API/*",
    "https://YOUR_REVIEW_API/*",
    "https://YOUR_LEAVES_PORTAL/*",
    "https://YOUR_REVIEW_PORTAL/*",
    "https://YOUR_SALARYSLIP_PORTAL/*"
  ],
  "background": {
    "service_worker": "background/service-worker.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "assets/icons/icon16.png",
      "48": "assets/icons/icon48.png",
      "128": "assets/icons/icon128.png"
    }
  },
  "options_page": "options/options.html",
  "content_scripts": [
    {
      "matches": ["http://localhost:4200/*", "https://YOUR_LEAVES_PORTAL/*"],
      "js": ["content/leaves-bridge.js"],
      "run_at": "document_idle"
    },
    {
      "matches": ["http://localhost:4201/*", "https://YOUR_REVIEW_PORTAL/*"],
      "js": ["content/review-bridge.js"],
      "run_at": "document_idle"
    },
    {
      "matches": ["https://YOUR_SALARYSLIP_PORTAL/*"],
      "js": ["content/salary-slip-bridge.js"],
      "run_at": "document_idle"
    }
  ],
  "externally_connectable": {
    "matches": [
      "http://localhost:4200/*",
      "http://localhost:4201/*",
      "https://YOUR_LEAVES_PORTAL/*",
      "https://YOUR_REVIEW_PORTAL/*",
      "https://YOUR_SALARYSLIP_PORTAL/*"
    ]
  }
}
```

**Permissions note:** `tabs` + `activeTab` are required for `chrome.tabs.captureVisibleTab`. Request explicit user consent on first enable in the options page.

---

## 4. Feature matrix

| Feature | Source system | Trigger | User action |
|---------|---------------|---------|-------------|
| **Unified login** | Salary-slip / Leaves OTP | Popup OTP or portal bridge | One session for all apps |
| **Day login** | Leaves | Alarm / popup button | `POST /attendance/login` |
| **Day logout** | Leaves | Alarm / popup button | `POST /attendance/logout` |
| **Task % complete** | Review L10 | Poll every 15 min | Popup progress bar; notify on delta ≥ 10% |
| **Meeting reminder** | Review `l10_teams` | Alarm T-15 min before meeting | Open review meeting prep |
| **Peer review due** | Review todos | Poll | Notify + badge |
| **Self-review due** | Review cycles | Poll | Notify + open portal |
| **Leave / shift alerts** | Leaves | Poll | Existing leave doc flows |
| **Random screenshot** | Extension → Leaves/Review API | Random interval during attendance day | Upload PNG for reports |

---

## 5. Unified session (SSO across applications)

All Gventure apps share the **same MySQL `employees` table** and **same `JWT_SECRET`** (see `backend/.env`). The extension is the **session broker**.

### 5.1 SessionManager (Singleton)

```typescript
// background/SessionManager.ts
export interface UnifiedSession {
  jwt: string;
  employeeId: string;       // employees.employee_id
  employeePk: number;       // employees.id (for L10 APIs)
  expiresAt: number;        // epoch ms
  sourceApp: 'extension' | 'leaves' | 'review' | 'salary-slip';
}

export class SessionManager {
  private static instance: SessionManager;

  static getInstance(): SessionManager {
    if (!SessionManager.instance) {
      SessionManager.instance = new SessionManager();
    }
    return SessionManager.instance;
  }

  async saveSession(session: UnifiedSession): Promise<void> {
    await chrome.storage.local.set({ unifiedSession: session });
    chrome.runtime.sendMessage({ type: 'SESSION_UPDATED' });
  }

  async getValidSession(): Promise<UnifiedSession | null> {
    const { unifiedSession } = await chrome.storage.local.get('unifiedSession');
    if (!unifiedSession || unifiedSession.expiresAt < Date.now()) return null;
    return unifiedSession;
  }

  async clearSession(): Promise<void> {
    await chrome.storage.local.remove('unifiedSession');
  }
}
```

### 5.2 Login flows

| Flow | Steps |
|------|-------|
| **A — Extension popup OTP** | User enters employee ID → extension calls salary-slip/leaves `POST /employee/request-otp` → user enters OTP → `POST /employee/login` → `SessionManager.saveSession` |
| **B — Portal bridge** | User logs into any portal → content script receives `GVENTURE_AUTH` message → extension stores same JWT |
| **C — Cross-app reuse** | User opens another portal → content script injects token into `sessionStorage` / dispatches auth event so SPA skips OTP |

### 5.3 Auth bridge protocol (all content scripts)

```typescript
// content/auth-bridge.ts — shared contract
const AUTH_MESSAGE = 'GVENTURE_AUTH';

window.addEventListener('message', (event) => {
  if (!allowedOrigins.includes(event.origin)) return;
  if (event.data?.type === AUTH_MESSAGE) {
    chrome.runtime.sendMessage({
      type: 'AUTH_FROM_PORTAL',
      payload: {
        jwt: event.data.token,
        employeeId: event.data.employeeId,
        sourceApp: event.data.app, // 'leaves' | 'review' | 'salary-slip'
      },
    });
  }
});

// Portal → extension (after login)
window.postMessage({
  type: 'GVENTURE_AUTH',
  token: jwt,
  employeeId,
  app: 'leaves',
}, window.location.origin);

// Extension → portal (on SESSION_UPDATED)
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === 'INJECT_SESSION') {
    window.postMessage({ type: 'GVENTURE_SESSION', token: msg.jwt, employeeId: msg.employeeId }, '*');
  }
});
```

Each Angular app adds a `GventureAuthBridgeService` (see [angular.md](../frondend/angular.md)) to emit and accept these messages.

### 5.4 Session rules

| Rule | Detail |
|------|--------|
| **Single source** | One `unifiedSession` key in `chrome.storage.local` |
| **Expiry** | Align with JWT `exp`; refresh via portal re-login or silent OTP if backend supports refresh tokens (v2) |
| **Logout** | Clears session everywhere; content scripts notify open tabs |
| **Security** | Strict origin allow-list; never expose JWT to arbitrary pages |

---

## 6. Attendance (day login / logout)

Unchanged core behaviour; tied to unified session.

### 6.1 AttendanceSyncService

```typescript
export class AttendanceSyncService {
  async recordLogin(): Promise<void> {
    const session = await SessionManager.getInstance().getValidSession();
    if (!session) throw new Error('Not logged in');

    await this.leavesApi.post('/attendance/login', {
      employeeId: session.employeeId,
      timestamp: new Date().toISOString(),
      source: 'chrome-extension',
    });

    await chrome.storage.local.set({ attendanceDayActive: true });
    await ScreenshotCaptureService.getInstance().startRandomSchedule();
  }

  async recordLogout(): Promise<void> {
    const session = await SessionManager.getInstance().getValidSession();
    if (!session) throw new Error('Not logged in');

    await this.leavesApi.post('/attendance/logout', {
      employeeId: session.employeeId,
      timestamp: new Date().toISOString(),
      source: 'chrome-extension',
    });

    await chrome.storage.local.set({ attendanceDayActive: false });
    await ScreenshotCaptureService.getInstance().stopSchedule();
  }
}
```

### 6.2 Alarms

| Alarm | Schedule | Action |
|-------|----------|--------|
| `day-start-reminder` | Shift start − 15 min | Notify → [Login now] |
| `day-end-reminder` | Shift start + 9 h | Notify → [Logout now] |
| `att-late-check` | Shift + 10 min grace | Warn if no login recorded |

---

## 7. Task progress updates (Review L10)

### 7.1 Data source

Poll **Review API** (NestJS, `/api/v1/l10` or `/api/v1/meetings`):

- `GET /l10/teams` — user's teams
- Per team: current-week todos (prep / member todos)
- Fields: `done`, `taskInfo`, `dueDate`, `needsPeerReview`, `peerReviewed`

### 7.2 Completion percentage

Computed **per employee, current ISO week**:

```typescript
function weeklyTaskCompletionPercent(todos: L10PrepTodo[]): number {
  const weekTodos = todos.filter(t => t.isThisPeriod !== false);
  if (weekTodos.length === 0) return 100;
  const done = weekTodos.filter(t => t.done).length;
  return Math.round((done / weekTodos.length) * 100);
}
```

Optional team rollup for managers: average of member percentages.

### 7.3 TaskProgressSyncService

| Behaviour | Detail |
|-----------|--------|
| **Poll interval** | Every 15 min (alarm `task-progress-sync`) |
| **State** | Store last known `%` in `chrome.storage.local.taskProgress` |
| **Notify** | If `%` increased by ≥ 10 points → desktop notification "Tasks: 60% → 75% complete this week" |
| **Popup** | Progress ring + list of open tasks (top 5 by due date) |
| **Badge** | Toolbar badge = count of incomplete todos due this week |

```typescript
chrome.alarms.create('task-progress-sync', { periodInMinutes: 15 });

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'task-progress-sync') {
    await TaskProgressSyncService.getInstance().sync();
  }
});
```

---

## 8. Meeting notifications

### 8.1 Data source

From Review / shared DB table `l10_teams`:

- `meeting_day_of_week` (0–6)
- `meeting_time` (HH:mm, team timezone)
- `l10_monthly_meeting_schedules` for monthly cadence

### 8.2 MeetingReminderService

| Notification | When | Message |
|--------------|------|---------|
| `meeting-prep-t24` | 24 h before | "Prepare L10 todos & rocks for tomorrow's meeting" |
| `meeting-start-t15` | 15 min before | "Platform team L10 starts at 10:00 — join prep" |
| `meeting-in-progress` | At start time | "Meeting in progress — complete your segue check-in" |

```typescript
// Schedule per team after login
for (const team of teams) {
  const next = computeNextMeetingInstant(team.meetingDayOfWeek, team.meetingTime, team.timezone);
  chrome.alarms.create(`meeting-${team.id}-t15`, { when: next - 15 * 60 * 1000 });
}
```

Deep link: `{reviewPortal}/l10/teams/{teamId}/meetings` or current meeting route.

---

## 9. Review notifications

### 9.1 Peer review

Poll todos where `needsPeerReview === true` and user is `assignedReviewerEmployeeId`:

| ID | Title | Trigger |
|----|-------|---------|
| `peer-review-due` | Peer review needed | Assigned reviewer, todo completed by teammate |
| `peer-review-overdue` | Review overdue | Due date passed, not `peerReviewed` |

### 9.2 Self-review & review cycles

Poll Review API:

- `GET /review-cycles/active` — open cycle for employee
- Self-review banner equivalent: notify if window open and submission incomplete

| ID | Title | Trigger |
|----|-------|---------|
| `self-review-open` | Self-review period open | Cycle status `open` |
| `self-review-deadline` | Self-review due soon | T-3 days before close |

### 9.3 ReviewAlertService

Runs on alarm `review-alert-sync` (every 30 min). Updates badge count for pending reviews (peer + self).

---

## 10. Random screenshot capture (activity reports)

During an **active attendance day** (after day login, before day logout), the extension captures screenshots on a **randomized schedule** for HR/management **activity reports** (productivity audit, remote work compliance).

### 10.1 ScreenshotCaptureService

```typescript
export class ScreenshotCaptureService {
  private nextCaptureAlarm: string | null = null;

  async startRandomSchedule(): Promise<void> {
    await this.scheduleNextCapture();
  }

  async stopSchedule(): Promise<void> {
    if (this.nextCaptureAlarm) chrome.alarms.clear(this.nextCaptureAlarm);
  }

  private async scheduleNextCapture(): Promise<void> {
    const { screenshotMinMinutes, screenshotMaxMinutes } = await this.getOptions();
    const delayMin = randomInt(screenshotMinMinutes, screenshotMaxMinutes);
    const name = `screenshot-${Date.now()}`;
    this.nextCaptureAlarm = name;
    chrome.alarms.create(name, { delayInMinutes: delayMin });
  }

  async captureAndUpload(): Promise<void> {
    const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
    if (!tab?.windowId) return;

    const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
      format: 'png',
      quality: 80,
    });

    const session = await SessionManager.getInstance().getValidSession();
    if (!session) return;

    const blob = dataUrlToBlob(dataUrl);
    await this.activityApi.uploadScreenshot({
      employeeId: session.employeeId,
      capturedAt: new Date().toISOString(),
      tabTitle: tab.title ?? '',
      tabUrl: sanitizeUrl(tab.url), // strip query tokens
      image: blob,
    });

    await this.scheduleNextCapture();
  }
}

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name.startsWith('screenshot-')) {
    await ScreenshotCaptureService.getInstance().captureAndUpload();
  }
});
```

### 10.2 Backend upload API (to implement)

Add to **Leaves** or **Review** backend (recommended: leaves `ActivityReportFacade`):

```
POST /api/v1/activity-reports/screenshots
Authorization: Bearer {jwt}
Content-Type: multipart/form-data

Fields: image (file), capturedAt, tabTitle, tabUrl (optional metadata)
```

Storage: object store or `activity_screenshots` table (employee_id, captured_at, storage_uri, blur_applied).  
Reports: batch job aggregates screenshots into HTML/PDF reports (similar to review `REPORT_STATIC_OUTPUT_DIR` pattern).

### 10.3 Privacy & compliance

| Control | Implementation |
|---------|----------------|
| **Opt-in** | Options toggle "Enable activity screenshots" (default off until HR policy accepted) |
| **Active day only** | No capture outside attendance login window |
| **URL sanitization** | Strip credentials, tokens, PII query params before upload |
| **Blocklist** | Never capture URLs matching `*bank*`, `*password*`, options blocklist |
| **Blur (v2)** | Optional client-side blur of non-work tabs |
| **Retention** | Server policy: e.g. 90 days, HR-only access |
| **Transparency** | Persistent popup indicator when capture is enabled |

---

## 11. Popup dashboard

| Section | Content |
|---------|---------|
| **Session** | Logged in as {name} · [Logout all apps] |
| **Attendance** | Today: not started / active / logged out · [Day Login] [Day Logout] |
| **Tasks this week** | Progress bar `{percent}%` · `{done}/{total}` todos |
| **Meetings** | Next L10: {team} · {datetime} |
| **Reviews** | `{n}` peer reviews pending · self-review status |
| **Leaves** | Balances + pending alerts (compact) |
| **Quick links** | Open Leaves · Review · Salary-slip |

If no session → inline OTP login (same as salary-slip flow).

---

## 12. Service worker alarms (summary)

| Alarm | Period | Service |
|-------|--------|---------|
| `day-start-reminder` | Daily | AttendanceSyncService |
| `day-end-reminder` | Daily | AttendanceSyncService |
| `sync-pending-items` | 30 min | Leaves pending (comp-off, shift, leave) |
| `task-progress-sync` | 15 min | TaskProgressSyncService |
| `review-alert-sync` | 30 min | ReviewAlertService |
| `meeting-*` | One-shot per team | MeetingReminderService |
| `screenshot-*` | Random 45–120 min | ScreenshotCaptureService (when attendance active) |

---

## 13. API clients

| Client | Base URL (dev) | Auth |
|--------|----------------|------|
| `LeavesApiClient` | `http://localhost:3000/api/v1` | Bearer JWT |
| `ReviewApiClient` | `http://localhost:4015/api/v1` | Bearer JWT (same secret) |
| `ActivityReportApiClient` | Leaves API | Bearer JWT |

All clients read token from `SessionManager.getValidSession()`.

---

## 14. Options page

| Setting | Default | Notes |
|---------|---------|-------|
| Enable day-start/end reminders | true | |
| Enable task progress notifications | true | |
| Enable meeting reminders | true | |
| Enable review notifications | true | |
| Enable activity screenshots | false | Requires HR policy acceptance |
| Screenshot interval min/max (minutes) | 45 / 120 | Random between |
| Quiet hours | 22:00–07:00 | Suppress non-urgent notifications |
| Leaves API URL | Config | |
| Review API URL | Config | |

---

## 15. Security

| Topic | Approach |
|-------|----------|
| **Unified JWT** | `chrome.storage.local`; shared secret across apps |
| **Origin checks** | All `postMessage` handlers validate origin |
| **Screenshots** | Opt-in; attendance-gated; URL sanitization; HR-only storage |
| **CORS** | Extension uses `host_permissions` |
| **Least privilege** | No `<all_urls>`; blocklisted domains for capture |

---

## 16. Notification catalog

| ID | Title | Source |
|----|-------|--------|
| `att-start` | Start your work day | Attendance |
| `att-end` | End your work day | Attendance |
| `att-late` | You may be marked late | Attendance |
| `task-progress` | Tasks: {old}% → {new}% complete | Review |
| `task-due-soon` | {n} tasks due this week | Review |
| `meeting-prep-t24` | Prepare for L10 meeting | Review |
| `meeting-start-t15` | L10 meeting in 15 minutes | Review |
| `peer-review-due` | Peer review needed | Review |
| `self-review-open` | Self-review period open | Review |
| `leave-approved` | Leave approved | Leaves |
| `shift-t5` | Submit next month shift | Leaves |
| `screenshot-captured` | (optional, dev only) | Extension |

---

## 17. Build & test

```bash
cd chrome-app
npm install
npm run build
```

Chrome → `chrome://extensions` → **Load unpacked** → `chrome-app/dist`.

| Test | Verify |
|------|--------|
| Unified login | OTP in popup → open leaves + review without re-login |
| Portal bridge | Login in review → extension session populated |
| Day login/logout | `attendance_events` row in DB |
| Task % | Popup matches review prep todo counts |
| Meeting alarm | Fire test alarm → notification with team name |
| Peer review | Badge when `needsPeerReview` todos exist |
| Screenshot | After login, alarm fires → upload API 201 |

---

## 18. Document history

| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-06-09 | Initial guide (attendance + leave alerts) |
| 1.1 | 2026-06-09 | Unified SSO, task %, meetings, reviews, random screenshots for reports |
