Skip to content

Feature/western bandicoot - #1

Merged
vaulttec-dev merged 4 commits into
mainfrom
feature/western-bandicoot
Jun 27, 2026
Merged

vaulttec-dev merged 4 commits into
mainfrom
feature/western-bandicoot

Conversation

@vaulttec-dev

@vaulttec-dev vaulttec-dev commented Jun 27, 2026 •

Copy link
Copy Markdown
Owner

Summary by cubic

Moves large uploads (Drive and Gemini) into the Meet page, adds crash-safe recording with recovery, and runs Gemini summarization in the background so you can close the tab. Improves reliability and reduces data shuffling for faster saves.

  • New Features

    • Crash-safe recording to IndexedDB with a recovery banner (save to Drive or download); auto cleanup after success or 7 days.
    • Direct, resumable Google Drive uploads from the page; local download fallback if Drive fails.
    • Separate audio track and background Gemini processing via chrome.alarms; you can close the tab; summary saved to the same folder as a Google Doc (fallback .txt).
    • Renamed the root Drive folder to “Meeting Recordings” with date/meeting subfolders.
  • Refactors

    • Large uploads happen in content.js; the service worker provides OAuth tokens (with refresh), badge/stop, and Gemini polling.
    • Shared helpers extracted to gdrive.js, gemini.js, and recstore.js.
    • manifest.json: added alarms; load recstore.js, gdrive.js, gemini.js before content.js.
    • README and CSS updated; IndexedDB bumped to v2 with rid_kind index for cheap duration estimates.

Written for commit 46e98f9. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Нові функції

    • Додано відновлення незавершеного запису з банером на сторінці Meet.
    • Покращено збереження записів у Drive та створення підсумків після зустрічі.
    • Оновлено структуру папок для записів із новою англомовною назвою та зрозумілішим шляхом.
  • Виправлення помилок

    • Підвищено надійність запису під час закриття вкладки або збою фону.
    • Зменшено ризик втрати останніх секунд запису та додано резервне локальне збереження.
  • Документація

    • Оновлено README з актуальним описом роботи та обмежень.

Greptile Summary

PR рефакторить архітектуру розширення: великі мережеві операції (Drive upload, Gemini upload) переміщено з service worker у content script, щоб уникнути передачі відео-blob через sendMessage. Фонова обробка конспектів тепер ведеться через chrome.alarms, що дозволяє закрити вкладку Meet після завершення запису.

  • Відмовостійкість: новий модуль recstore.js (IndexedDB) пише шматки MediaRecorder на диск під час запису; при аварійному завершенні вкладки записані дані відновлюються банером при наступному відкритті Meet.
  • Розділення відповідальності: логіку Drive (gdrive.js) і Gemini (gemini.js) винесено в окремі чисті модулі, спільні для content script і service worker без дублювання.
  • Аудіо-доріжка для Gemini: замість відео-файлу в Gemini тепер надсилається тільки окремий аудіо-трек (opus), що суттєво зменшує розмір завантаження та споживання токенів.

Confidence Score: 3/5

Нова функція відновлення містить два дефекти в content.js — перед мерджем їх слід усунути.

Механізм відновлення у listOrphans не розрізняє активні та осиротілі сесії — сценарій з двома вкладками Meet дає банер відновлення поверх живого запису з можливістю видалити IndexedDB-дані активної сесії. Другий дефект: при відновленні старих сесій без аудіо-доріжки відео-blob надсилається в Gemini з MIME-типом audio/webm, що призводить до помилки генерації конспекту.

content.js — функції initRecovery, recover та maybeGemini; recstore.js — listOrphans.

Important Files Changed

Filename Overview
content.js Значно розширено: IndexedDB-журнал, окрема аудіо-доріжка для Gemini, прямий Drive-аплоад і механізм відновлення. Два дефекти: конфлікт відновлення між вкладками та неправильний MIME-тип при fallback до відео-blob.
recstore.js Новий модуль IndexedDB: зберігання шматків, підрахунок тривалості, видалення курсором. Логіка транзакцій коректна.
gdrive.js Новий модуль Drive-функцій. Resumable upload повертає folderId. Санітизація folder-name неповна (не екрановано зворотні слеші).
gemini.js Новий модуль Gemini: upload, getFile, generateContent. Логіка коректна, maxOutputTokens збільшено до 16384.
background.js Значно спрощено: збережено OAuth, badge, STOP-relay і Gemini-polling через chrome.alarms з geminiBusy-guard.
manifest.json Додано дозвіл alarms і нові content-scripts у правильному порядку.

Fix All in Cursor

Reviews (1): Last reviewed commit: "Enhance audio recording capabilities in ..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

Context used:

  • Context used - Усі коментарі, описи та діаграми створюй виключно ... (source)

…ad functionality. Implemented OAuth token handling in the service worker, enabling direct uploads to Google Drive and Gemini from the content script. Updated README to clarify the new architecture and functionality, including large uploads and background processing. Adjusted permissions in manifest.json to support new features.
…mented a recovery banner to restore unsaved sessions from IndexedDB, allowing users to save or download recordings after a crash. Updated CSS for the recovery banner styling and modified content.js to handle recovery logic. Enhanced README to document the new recovery functionality.
…n background.js and gdrive.js. Modify README to reflect the new folder structure for meeting recordings in Google Drive.
…oduced separate audio track recording alongside video, allowing for efficient audio uploads to Gemini. Updated data handling in IndexedDB to support audio chunks and modified upload logic to accommodate audio files. Incremented database version to v2 for improved chunk indexing. Updated README to reflect new audio features and changes in functionality.
Copilot AI review requested due to automatic review settings June 27, 2026 20:12
@vaulttec-dev
vaulttec-dev merged commit f21355f into main Jun 27, 2026
2 of 3 checks passed
@coderabbitai

coderabbitai Bot commented Jun 27, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2470b184-771b-4c9e-b642-5518a118073b

📥 Commits

Reviewing files that changed from the base of the PR and between 58a9fdf and 46e98f9.

📒 Files selected for processing (8)
  • README.md
  • background.js
  • content.css
  • content.js
  • gdrive.js
  • gemini.js
  • manifest.json
  • recstore.js

📝 Walkthrough

Walkthrough

Розширення для запису Google Meet суттєво переписано: додано новий модуль recstore.js для IndexedDB-журналювання медіа-чанків, окремий аудіо-рекордер у content script, модулі gdrive.js і gemini.js з helper-функціями, фонову Gemini-обробку через chrome.alarms замість inline-логіки, OAuth-проксі через service worker та UI-банер відновлення урваних записів.

Changes

Meet Recorder — повний рефакторинг

Layer / File(s) Summary
IndexedDB-модуль RecStore
recstore.js
Новий модуль реалізує запис/читання медіа-чанків (video/audio) в IndexedDB: управління сесіями, підрахунок, список orphan-сесій, видалення та автоочистка. Експортується як globalThis.RecStore.
Нові модулі gdrive.js та gemini.js
gdrive.js, gemini.js
gdrive.js надає функції для Google Drive: пошук/створення папок, resumable upload відео, multipart-створення Doc. gemini.js реалізує resumable upload до Gemini Files API, отримання стану файлу та генерацію конспекту.
Маніфест: нові дозволи та скрипти
manifest.json
До permissions додано alarms; у content_scripts поруч з content.js додано recstore.js, gdrive.js, gemini.js.
Service worker: alarm-polling Gemini та OAuth-проксі
background.js
Додано GET_TOKEN/REFRESH_TOKEN для content script; GEMINI_CONTINUE запускає startGeminiJob; pollGeminiJob через chrome.alarms.onAlarm обробляє стани Gemini (PROCESSING/ACTIVE/FAILED); saveDoc створює Google Doc у Drive з fallback на локальний .txt; finishGeminiJob завершує цикл.
Content script: двопотоковий запис, RecStore та recovery-banner
content.js, content.css
Додано окремий audioRecorder, OAuth через withToken, запис чанків у RecStore, реконструкцію блобів при зупинці, Drive resumable upload з fallback, maybeGemini для фонової обробки аудіо, initRecovery з recovery-banner та recover() з підтримкою Drive/локального відновлення.
README: оновлена документація
README.md
Змінено назву Drive-папки, переписано секцію архітектури з новими ролями компонентів, розширено опис відмовостійкості через IndexedDB та скориговано перелік обмежень.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ContentScript
  participant RecStore as RecStore (IndexedDB)
  participant ServiceWorker
  participant DriveAPI as Google Drive API
  participant GeminiAPI as Gemini Files API

  rect rgba(70, 130, 180, 0.5)
    Note over ContentScript,RecStore: Запис
    User->>ContentScript: startCapture()
    ContentScript->>RecStore: startSession(meta)
    ContentScript->>ContentScript: старт videoRecorder + audioRecorder (1s chunks)
    loop ondataavailable
      ContentScript->>RecStore: appendChunk(video) / appendChunk(audio)
    end
  end

  rect rgba(60, 179, 113, 0.5)
    Note over ContentScript,DriveAPI: Збереження
    User->>ContentScript: stopCapture()
    ContentScript->>RecStore: readBlob(video) + readBlob(audio)
    ContentScript->>ServiceWorker: GET_TOKEN
    ServiceWorker-->>ContentScript: OAuth token
    ContentScript->>DriveAPI: uploadResumable(videoBlob) → fileId, folderId
    ContentScript->>GeminiAPI: geminiUploadFile(audioBlob) → fileUri
    ContentScript->>ServiceWorker: GEMINI_CONTINUE(job)
  end

  rect rgba(255, 165, 0, 0.5)
    Note over ServiceWorker,DriveAPI: Фонова Gemini-обробка
    loop chrome.alarms (кожну хвилину)
      ServiceWorker->>GeminiAPI: geminiGetFile(fileUri)
      alt ACTIVE
        ServiceWorker->>GeminiAPI: geminiGenerate(fileUri) → текст
        ServiceWorker->>DriveAPI: createDriveDoc(text)
        ServiceWorker->>ServiceWorker: finishGeminiJob(done)
      else PROCESSING
        ServiceWorker->>ServiceWorker: ticks++
      else FAILED / timeout
        ServiceWorker->>ServiceWorker: finishGeminiJob(error)
      end
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 Стрибнув я у код — там IndexedDB,
Два потоки пишуть чанки в тиші глибокій,
Аларм кожну хвилину — Gemini не спить,
Drive-папка "Meeting Recordings" вже блищить,
А банер відновлення — рятує все сміло,
Кролик пишається: зроблено діло! 🎉

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/western-bandicoot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vaulttec-dev
vaulttec-dev deleted the feature/western-bandicoot branch June 27, 2026 20:12
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Move Drive/Gemini uploads to content script, add crash recovery + audio-only Gemini
✨ Enhancement 🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Description

• Upload recordings to Google Drive and Gemini directly from the Meet tab for large-file
 reliability.
• Add IndexedDB chunk journaling and an in-page recovery banner for interrupted recordings.
• Generate Gemini summaries from a separate audio-only track, with background polling via alarms.
Diagram

graph TD
  meet["Meet tab"] --> content["content.js"] --> recstore[("IndexedDB (RecStore)")]
  content -- "GET_TOKEN/REFRESH" --> bg["background.js (SW)" ] -- "OAuth token" --> content
  content -- "resumable upload" --> drive{{"Google Drive API"}}
  content -- "audio upload" --> gemini{{"Gemini Files API"}}
  bg -- "poll + generate" --> gemini -- "markdown summary" --> bg -- "create doc" --> drive

  subgraph Legend
    direction LR
    _s["Script / component"] ~~~ _db[("Database")] ~~~ _e{{"External API"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep uploads in service worker using streaming/chunked messaging
  • ➕ Centralizes network operations and credentials handling in one place
  • ➕ Avoids running large network transfers in a page context
  • ➖ Chrome extension messaging makes multi-GB transfers brittle/complex
  • ➖ More custom protocol code (chunking, backpressure, retries) versus direct fetch
2. Use Offscreen Document for recording + uploads
  • ➕ More isolated execution environment than the Meet page DOM
  • ➕ Can survive some tab lifecycle events better than a content script
  • ➖ More moving parts (offscreen lifecycle, routing streams)
  • ➖ Still needs careful handling for large uploads and user-gesture capture constraints
3. Persist final recording via File System Access API (instead of IndexedDB chunks)
  • ➕ Writes directly to disk without accumulating DB entries
  • ➕ Potentially more efficient for very long sessions
  • ➖ Permission and UX complexity (user prompts/handles)
  • ➖ Not consistently available/usable in extension content script contexts

Recommendation: The chosen approach (large uploads in content script + token vending and Gemini polling in the service worker) is the most pragmatic for Chrome extension constraints: it avoids sendMessage size/latency issues and uses chrome.alarms to keep Gemini processing resilient to tab closure. The IndexedDB chunk journal is a reasonable tradeoff to add crash recovery without introducing a heavier offscreen/streaming architecture.

Files changed (8) +757 / -329

Enhancement (6) +725 / -313
background.jsRefocus service worker on OAuth token vending and Gemini background polling +126/-291

Refocus service worker on OAuth token vending and Gemini background polling

• Removes direct save/upload orchestration and instead exposes message handlers for badge/state, stop forwarding, and token issuance/refresh for the content script. Adds alarm-driven Gemini job polling to generate summaries asynchronously and save them to Drive (or local .txt fallback). Imports shared GDrive/Gemini helper modules.

background.js

content.cssAdd styling for interrupted-recording recovery banner +39/-0

Add styling for interrupted-recording recovery banner

• Introduces fixed-position banner styling with action buttons to recover or dismiss an unfinished recording session. Ensures banner is highly visible via z-index and Meet-aligned typography.

content.css

content.jsDirect Drive/Gemini uploads, audio-only Gemini track, and IndexedDB recovery flow +228/-22

Direct Drive/Gemini uploads, audio-only Gemini track, and IndexedDB recovery flow

• Moves Drive upload to resumable fetch calls from the Meet tab, requesting OAuth tokens from the service worker with a 401 refresh path. Adds parallel audio-only MediaRecorder track for Gemini summarization and hands off a background Gemini job to the service worker. Implements chunk journaling via RecStore and adds a recovery banner to restore and save/download interrupted sessions.

content.js

gdrive.jsIntroduce shared token-driven Google Drive helper module +100/-0

Introduce shared token-driven Google Drive helper module

• Adds pure functions for folder creation/lookup, meeting-folder structure, resumable uploads, and Google Doc creation. Standardizes the Drive root folder name to "Meeting Recordings" and returns folderId for reuse by the summarization flow.

gdrive.js

gemini.jsIntroduce shared Gemini helper module with resumable upload + generation calls +112/-0

Introduce shared Gemini helper module with resumable upload + generation calls

• Adds pure functions for uploading media to Gemini Files API, checking file state, and generating a markdown summary. Updates the prompt to target audio-only summarization and raises max output tokens to support longer summaries.

gemini.js

recstore.jsAdd IndexedDB-based chunk journal for crash-safe recording recovery +120/-0

Add IndexedDB-based chunk journal for crash-safe recording recovery

• Implements an IndexedDB schema for recording sessions and per-second chunks, including a v2 migration adding a composite index for efficient duration estimation. Provides APIs to start sessions, append chunks (video/audio), rebuild a Blob, list orphan sessions, prune old sessions, and delete sessions with their chunks.

recstore.js

Documentation (1) +30 / -14
README.mdDocument new upload architecture, recovery, and Drive folder naming +30/-14

Document new upload architecture, recovery, and Drive folder naming

• Updates the architecture description to reflect direct Drive/Gemini uploads from the content script with OAuth token vending from the service worker. Documents crash recovery via IndexedDB chunk journaling and the recovery banner workflow. Renames the Drive root folder reference to "Meeting Recordings".

README.md

Other (1) +2 / -2
manifest.jsonEnable alarms and load shared + storage modules in Meet content scripts +2/-2

Enable alarms and load shared + storage modules in Meet content scripts

• Adds the "alarms" permission to support background Gemini polling in the service worker. Registers recstore.js, gdrive.js, and gemini.js to load before content.js on meet.google.com.

manifest.json

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Втрачений останній chunk 🐞 Bug ≡ Correctness
Description
У content.js chunk’и пишуться в IndexedDB через RecStore.appendChunk() без очікування завершення,
але onRecorderStop() одразу збирає Blob через RecStore.readBlob(), тому останній(і) chunk(и) можуть
не потрапити у фінальний файл. Після успішного збереження код видаляє сесію з IndexedDB, роблячи
таку втрату незворотною.
Code

content.js[R142-146]

+      recorder.ondataavailable = (e) => {
+        if (!e.data || !e.data.size) return;
+        if (recId) RecStore.appendChunk(recId, e.data, 'video').catch((err) => console.warn('[MeetRec] appendChunk:', err));
+        else chunks.push(e.data);
+      };
Evidence
Виклики RecStore.appendChunk(...) запускають асинхронні IndexedDB-транзакції, але код не очікує їх
завершення перед readBlob()/deleteSession(), що створює гонку та ризик пропуску останніх
записаних шматків.

content.js[142-146]
content.js[190-213]
recstore.js[67-72]
recstore.js[39-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`RecStore.appendChunk()` викликається асинхронно (і не очікується), а фіналізація (`readBlob()` + `deleteSession()`) може стартувати раніше, ніж закомічено останні транзакції запису chunk’ів. Це дає ризик тихої усіченості кінця запису.

### Issue Context
- `RecStore.appendChunk()` є `async` і завершується лише після `tx(...).oncomplete`.
- У `ondataavailable` проміси не трекаються, отже `onRecorderStop()` не має гарантії, що всі записи в IndexedDB завершені.

### Fix Focus Areas
- content.js[142-146]
- content.js[190-224]
- recstore.js[67-72]

### Suggested approach
1. Збирати проміси запису chunk’ів у масив/чергу (наприклад, `pendingWrites.push(RecStore.appendChunk(...))`).
2. У `onRecorderStop()` (перед `readBlob()` і тим паче перед `deleteSession()`) виконати `await Promise.allSettled(pendingWrites)` (окремо для відео й аудіо або спільно).
3. Аналогічно забезпечити flush для аудіо-доріжки (подія `audioRecorder.stop` не гарантує commit IndexedDB-записів без await).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Неперевірений локальний фолбек 🐞 Bug ☼ Reliability
Description
saveRecording() у фолбеку на локальне збереження одразу повертає {where:'local'}, не перевіряючи,
що downloadLocally() реально відпрацював без помилки. Далі onRecorderStop() може видалити сесію з
IndexedDB, тож у випадку збою локального завантаження журнал для відновлення вже втрачено.
Code

content.js[R228-236]

+  async function saveRecording(blob, name) {
+    try {
+      const { folderId } = await withToken((token) => GDrive.uploadResumable(token, blob, name));
+      return { where: 'drive', folderId };
+    } catch (driveErr) {
+      console.warn('[MeetRec] Drive недоступний, зберігаю локально:', driveErr);
+      downloadLocally(blob, name);
+      return { where: 'local', folderId: null };
+    }
Evidence
У фолбек-гілці Drive помилки локальне збереження запускається "best-effort", але результат не
контролюється; при цьому інший код може прибрати дані відновлення з IndexedDB.

content.js[206-214]
content.js[228-236]
content.js[239-249]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Фолбек "локально" не має сигналу успіху/помилки, але потік поводиться так, ніби збереження гарантовано відбулося. Це може призвести до втрати можливості відновлення з IndexedDB.

### Issue Context
- `downloadLocally()` не повертає статус/проміс.
- `onRecorderStop()` видаляє сесію з IndexedDB після `saveRecording()`.

### Fix Focus Areas
- content.js[206-214]
- content.js[228-249]

### Suggested approach
- Обгорнути `downloadLocally()` в try/catch і при винятку кидати помилку (щоб сесію НЕ видаляти).
- Розглянути зміну логіки: видаляти сесію з IndexedDB лише після підтвердженої успішності хоча б одного каналу збереження (Drive або локального), або лишати сесію до явної дії користувача (як у recovery-банері).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. readBlob читає зайве 🐞 Bug ➹ Performance
Description
RecStore.readBlob() завжди робить getAll() по rid і лише потім фільтрує за kind, тому при
читанні відео в пам’ять підтягуються й аудіо-blob’и (і навпаки). На довгих записах це суттєво
підвищує пікове споживання пам’яті під час фіналізації/відновлення.
Code

recstore.js[R74-81]

+  // Зібрати всі шматки однієї доріжки сесії в один Blob (за порядком вставки).
+  // Старі записи без поля kind трактуємо як 'video'.
+  async function readBlob(id, mime, kind = 'video') {
+    const parts = await tx('chunks', 'readonly', (t) =>
+      reqDone(t.objectStore('chunks').index('rid').getAll(IDBKeyRange.only(id))));
+    const wanted = (parts || []).filter((p) => (p.kind || 'video') === kind);
+    return new Blob(wanted.map((p) => p.blob), { type: mime || 'video/webm' });
+  }
Evidence
readBlob() використовує індекс rid і тягне всі chunks, тоді як rid_kind вже створений і
дозволяє адресно читати лише потрібну доріжку.

recstore.js[74-81]
recstore.js[83-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`readBlob()` читає всі chunks сесії незалежно від доріжки, а потім відкидає непотрібні. Це зайві I/O та пам’ять.

### Issue Context
Є індекс `rid_kind`, але він використовується лише в `countChunks()`.

### Fix Focus Areas
- recstore.js[74-90]

### Suggested approach
- Для нових сесій читати через `index('rid_kind')` для потрібного `kind`.
- Для зворотної сумісності зі старими записами (без `kind`) можна:
 - при `kind==='video'` додатково дочитувати legacy-записи (де `kind` відсутній) або
 - міграцією/оновленням при першому читанні проставляти `kind` для існуючих chunks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Gemini job перезаписується 🐞 Bug ☼ Reliability
Description
Service worker зберігає лише один geminiJob у chrome.storage.local, і кожен GEMINI_CONTINUE
перезаписує попередній. Оскільки обробка може тривати до ~30 хв, новий запис/конспект, запущений
раніше завершення попереднього, може “витіснити” попередній job і залишити його без конспекту.
Code

background.js[R103-118]

+    case 'GEMINI_CONTINUE':
+      // content залив відео в Gemini → ведемо дрібну обробку у фоні (alarms).
+      startGeminiJob(msg.job)
+        .then(() => sendResponse({ ok: true }))
+        .catch((e) => sendResponse({ ok: false, error: e.message }));
+      return true;
+  }
+});

-// Створити Google Doc із конспекту в тій самій теці (multipart → конвертація в Google Doc).
-async function createDriveDoc(token, folderId, name, text) {
-  const boundary = 'meetrec_doc_boundary';
-  const meta = JSON.stringify({
-    name,
-    parents: [folderId],
-    mimeType: 'application/vnd.google-apps.document'
-  });
-  const body = new Blob([
-    `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n`,
-    meta,
-    `\r\n--${boundary}\r\nContent-Type: text/markdown; charset=UTF-8\r\n\r\n`,
-    text,
-    `\r\n--${boundary}--`
-  ], { type: `multipart/related; boundary=${boundary}` });
+// ---- Фонова Gemini-обробка (переживає засинання SW через chrome.alarms) ----
+// job = { geminiFileName, fileUri, mimeType, docName, meetingBaseName, folderId }

-  const r = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', {
-    method: 'POST',
-    headers: { Authorization: `Bearer ${token}` },
-    body
-  });
-  if (!r.ok) throw httpError('doc create', r.status);
-  return r.json();
+async function startGeminiJob(job) {
+  await chrome.storage.local.set({ geminiJob: { ...job, ticks: 0 } });
+  setStatus('Роблю конспект через Gemini…');
+  await chrome.alarms.create(GEMINI_ALARM, { periodInMinutes: 0.5 });
Evidence
content script ініціює новий job після кожного запису, а SW зберігає job одним ключем і перетирає
його при наступному старті.

background.js[103-119]
content.js[251-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`geminiJob` — одиночний слот. Нові job’и замінюють старі, що призводить до втрати фонового конспекту при кількох записах підряд.

### Issue Context
- content script завжди відправляє `GEMINI_CONTINUE` після зупинки запису.
- SW тримає лише один job і один alarm.

### Fix Focus Areas
- background.js[103-119]
- background.js[129-163]
- content.js[251-273]

### Suggested approach
- Зберігати масив job’ів (`geminiJobs: []`) і обробляти послідовно (FIFO):
 1) якщо черга порожня — стартувати alarm,
 2) якщо не порожня — додати job і повернути успіх + статус "у черзі".
- Або явно відхиляти новий job, якщо вже є активний (і показати користувачу зрозумілий статус).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Interactive OAuth у alarms 🐞 Bug ☼ Reliability
Description
saveDoc() виконується з alarm-тіка, але withFreshToken() завжди викликає
getAuthToken({interactive:true}). У станах, коли потрібна взаємодія користувача (повторна
згода/логін), це може зірвати фонове збереження в Drive і змусити частіше падати в локальний .txt
фолбек.
Code

background.js[R29-32]

+// Виконати fn(token); якщо токен прострочений (401) — скинути з кешу й повторити раз.
async function withFreshToken(fn) {
  let token = await getToken(true);
  try {
Evidence
withFreshToken() завжди стартує interactive-токен, а saveDoc() викликається з alarm-обробника,
тобто потенційно без user gesture.

background.js[29-42]
background.js[121-123]
background.js[165-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Фоновий шлях (chrome.alarms) не повинен безумовно вимагати interactive OAuth. Це знижує надійність фонової публікації конспекту в Drive.

### Issue Context
`pollGeminiJob()` запускається через `chrome.alarms`, далі викликає `saveDoc()`, яка всередині використовує `withFreshToken()`.

### Fix Focus Areas
- background.js[29-42]
- background.js[121-123]
- background.js[165-177]

### Suggested approach
- Додати параметр/режим у `withFreshToken(fn, {interactive})`.
- Для alarm-шляху робити `getToken(false)`; якщо токена немає — завершувати job зі статусом "потрібна авторизація для Drive" або відкладати до наступної user-driven дії.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread content.js
Comment on lines +142 to +146
recorder.ondataavailable = (e) => {
if (!e.data || !e.data.size) return;
if (recId) RecStore.appendChunk(recId, e.data, 'video').catch((err) => console.warn('[MeetRec] appendChunk:', err));
else chunks.push(e.data);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Втрачений останній chunk 🐞 Bug ≡ Correctness

У content.js chunk’и пишуться в IndexedDB через RecStore.appendChunk() без очікування завершення,
але onRecorderStop() одразу збирає Blob через RecStore.readBlob(), тому останній(і) chunk(и) можуть
не потрапити у фінальний файл. Після успішного збереження код видаляє сесію з IndexedDB, роблячи
таку втрату незворотною.
Agent Prompt
### Issue description
`RecStore.appendChunk()` викликається асинхронно (і не очікується), а фіналізація (`readBlob()` + `deleteSession()`) може стартувати раніше, ніж закомічено останні транзакції запису chunk’ів. Це дає ризик тихої усіченості кінця запису.

### Issue Context
- `RecStore.appendChunk()` є `async` і завершується лише після `tx(...).oncomplete`.
- У `ondataavailable` проміси не трекаються, отже `onRecorderStop()` не має гарантії, що всі записи в IndexedDB завершені.

### Fix Focus Areas
- content.js[142-146]
- content.js[190-224]
- recstore.js[67-72]

### Suggested approach
1. Збирати проміси запису chunk’ів у масив/чергу (наприклад, `pendingWrites.push(RecStore.appendChunk(...))`).
2. У `onRecorderStop()` (перед `readBlob()` і тим паче перед `deleteSession()`) виконати `await Promise.allSettled(pendingWrites)` (окремо для відео й аудіо або спільно).
3. Аналогічно забезпечити flush для аудіо-доріжки (подія `audioRecorder.stop` не гарантує commit IndexedDB-записів без await).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread content.js
Comment on lines +228 to +236
async function saveRecording(blob, name) {
try {
const { folderId } = await withToken((token) => GDrive.uploadResumable(token, blob, name));
return { where: 'drive', folderId };
} catch (driveErr) {
console.warn('[MeetRec] Drive недоступний, зберігаю локально:', driveErr);
downloadLocally(blob, name);
return { where: 'local', folderId: null };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Неперевірений локальний фолбек 🐞 Bug ☼ Reliability

saveRecording() у фолбеку на локальне збереження одразу повертає {where:'local'}, не перевіряючи,
що downloadLocally() реально відпрацював без помилки. Далі onRecorderStop() може видалити сесію з
IndexedDB, тож у випадку збою локального завантаження журнал для відновлення вже втрачено.
Agent Prompt
### Issue description
Фолбек "локально" не має сигналу успіху/помилки, але потік поводиться так, ніби збереження гарантовано відбулося. Це може призвести до втрати можливості відновлення з IndexedDB.

### Issue Context
- `downloadLocally()` не повертає статус/проміс.
- `onRecorderStop()` видаляє сесію з IndexedDB після `saveRecording()`.

### Fix Focus Areas
- content.js[206-214]
- content.js[228-249]

### Suggested approach
- Обгорнути `downloadLocally()` в try/catch і при винятку кидати помилку (щоб сесію НЕ видаляти).
- Розглянути зміну логіки: видаляти сесію з IndexedDB лише після підтвердженої успішності хоча б одного каналу збереження (Drive або локального), або лишати сесію до явної дії користувача (як у recovery-банері).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread recstore.js
Comment on lines +74 to +81
// Зібрати всі шматки однієї доріжки сесії в один Blob (за порядком вставки).
// Старі записи без поля kind трактуємо як 'video'.
async function readBlob(id, mime, kind = 'video') {
const parts = await tx('chunks', 'readonly', (t) =>
reqDone(t.objectStore('chunks').index('rid').getAll(IDBKeyRange.only(id))));
const wanted = (parts || []).filter((p) => (p.kind || 'video') === kind);
return new Blob(wanted.map((p) => p.blob), { type: mime || 'video/webm' });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Readblob читає зайве 🐞 Bug ➹ Performance

RecStore.readBlob() завжди робить getAll() по rid і лише потім фільтрує за kind, тому при
читанні відео в пам’ять підтягуються й аудіо-blob’и (і навпаки). На довгих записах це суттєво
підвищує пікове споживання пам’яті під час фіналізації/відновлення.
Agent Prompt
### Issue description
`readBlob()` читає всі chunks сесії незалежно від доріжки, а потім відкидає непотрібні. Це зайві I/O та пам’ять.

### Issue Context
Є індекс `rid_kind`, але він використовується лише в `countChunks()`.

### Fix Focus Areas
- recstore.js[74-90]

### Suggested approach
- Для нових сесій читати через `index('rid_kind')` для потрібного `kind`.
- Для зворотної сумісності зі старими записами (без `kind`) можна:
  - при `kind==='video'` додатково дочитувати legacy-записи (де `kind` відсутній) або
  - міграцією/оновленням при першому читанні проставляти `kind` для існуючих chunks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread background.js
Comment on lines +103 to +118
case 'GEMINI_CONTINUE':
// content залив відео в Gemini → ведемо дрібну обробку у фоні (alarms).
startGeminiJob(msg.job)
.then(() => sendResponse({ ok: true }))
.catch((e) => sendResponse({ ok: false, error: e.message }));
return true;
}
});

// Створити Google Doc із конспекту в тій самій теці (multipart → конвертація в Google Doc).
async function createDriveDoc(token, folderId, name, text) {
const boundary = 'meetrec_doc_boundary';
const meta = JSON.stringify({
name,
parents: [folderId],
mimeType: 'application/vnd.google-apps.document'
});
const body = new Blob([
`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n`,
meta,
`\r\n--${boundary}\r\nContent-Type: text/markdown; charset=UTF-8\r\n\r\n`,
text,
`\r\n--${boundary}--`
], { type: `multipart/related; boundary=${boundary}` });
// ---- Фонова Gemini-обробка (переживає засинання SW через chrome.alarms) ----
// job = { geminiFileName, fileUri, mimeType, docName, meetingBaseName, folderId }

const r = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body
});
if (!r.ok) throw httpError('doc create', r.status);
return r.json();
async function startGeminiJob(job) {
await chrome.storage.local.set({ geminiJob: { ...job, ticks: 0 } });
setStatus('Роблю конспект через Gemini…');
await chrome.alarms.create(GEMINI_ALARM, { periodInMinutes: 0.5 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Gemini job перезаписується 🐞 Bug ☼ Reliability

Service worker зберігає лише один geminiJob у chrome.storage.local, і кожен GEMINI_CONTINUE
перезаписує попередній. Оскільки обробка може тривати до ~30 хв, новий запис/конспект, запущений
раніше завершення попереднього, може “витіснити” попередній job і залишити його без конспекту.
Agent Prompt
### Issue description
`geminiJob` — одиночний слот. Нові job’и замінюють старі, що призводить до втрати фонового конспекту при кількох записах підряд.

### Issue Context
- content script завжди відправляє `GEMINI_CONTINUE` після зупинки запису.
- SW тримає лише один job і один alarm.

### Fix Focus Areas
- background.js[103-119]
- background.js[129-163]
- content.js[251-273]

### Suggested approach
- Зберігати масив job’ів (`geminiJobs: []`) і обробляти послідовно (FIFO):
  1) якщо черга порожня — стартувати alarm,
  2) якщо не порожня — додати job і повернути успіх + статус "у черзі".
- Або явно відхиляти новий job, якщо вже є активний (і показати користувачу зрозумілий статус).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Meet recorder extension so recording is resilient to tab/browser crashes by persisting MediaRecorder chunks to IndexedDB, and it moves large uploads (Drive + Gemini file upload) into the content script to avoid passing large blobs through extension messaging.

Changes:

  • Add an IndexedDB-backed recording journal (recstore.js) and a recovery banner UI to resume unfinished recordings.
  • Introduce shared “pure” API helpers for Google Drive and Gemini (gdrive.js, gemini.js) and adjust background/content responsibilities.
  • Update MV3 permissions and documentation to match the new flow (alarms-based background polling for Gemini).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
recstore.js New IndexedDB journal for chunk persistence, session listing/pruning, and recovery support.
content.js Writes chunks to RecStore, uploads directly to Drive, uploads media to Gemini, and adds recovery banner/actions.
background.js Supplies OAuth tokens to content script and runs Gemini polling via chrome.alarms to survive SW sleep.
gdrive.js New shared Drive helpers (folder creation, resumable upload, Google Doc creation).
gemini.js New shared Gemini helpers (resumable file upload, status polling request, generateContent).
manifest.json Adds alarms permission and loads new helper scripts before content.js.
content.css Styles for the new “unfinished recording” recovery banner.
README.md Updates architecture description and adds a resiliency section (needs alignment with actual Gemini media).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread content.js
Comment on lines +209 to +213
setStatus(saved.where === 'drive'
? 'Готово ✓ — збережено в Google Drive'
: 'Готово ✓ — збережено локально (тека «Завантаження»)');
if (id) await RecStore.deleteSession(id); // відео в безпеці → журнал більше не потрібен
await maybeGemini(audioBlob, name, saved.folderId); // ставить власні статуси
Comment thread content.js
Comment on lines +342 to +344
const audioBlob = await RecStore.readBlob(session.id, 'audio/webm', 'audio').catch(() => null);
await RecStore.deleteSession(session.id);
await maybeGemini(audioBlob && audioBlob.size ? audioBlob : blob, name, saved.folderId);
Comment thread content.js
Comment on lines +346 to +349
downloadLocally(blob, name);
setStatus('Відновлено ✓ — файл завантажується');
await RecStore.deleteSession(session.id);
}
Comment thread recstore.js
Comment on lines +12 to +37
function openDb() {
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains('recordings')) {
db.createObjectStore('recordings', { keyPath: 'id' });
}
let chunks;
if (!db.objectStoreNames.contains('chunks')) {
// autoIncrement → монотонний порядок вставки; індекс 'rid' групує шматки сесії.
chunks = db.createObjectStore('chunks', { keyPath: 'seq', autoIncrement: true });
chunks.createIndex('rid', 'rid', { unique: false });
} else {
chunks = req.transaction.objectStore('chunks');
}
if (!chunks.indexNames.contains('rid_kind')) {
chunks.createIndex('rid_kind', ['rid', 'kind'], { unique: false });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error || new Error('indexedDB open failed'));
});
return dbPromise;
}
Comment thread background.js
Comment on lines +141 to +143
if (job.ticks >= GEMINI_MAX_TICKS) {
await finishGeminiJob('Конспект не вдалося зробити: тайм-аут обробки відео');
} else {
Comment thread background.js
Comment on lines +148 to +151
if (file.state === 'FAILED') {
await finishGeminiJob('Конспект не вдалося зробити: Gemini не обробив відео');
return;
}
Comment thread README.md
Comment on lines 92 to +97
Технічна примітка: надсилається саме відео (`video/webm`) з `mediaResolution: LOW` — бо
MediaRecorder дає `webm/opus`, який Gemini не приймає як аудіо, а конвертація в service
worker неможлива (немає Web Audio API). Низька роздільність кадрів тримає запит у межах
лімітів навіть для довгих зустрічей.
`MediaRecorder` дає `webm/opus`, який Gemini не приймає як чисте аудіо. Відео заливається в
Gemini Files API прямо з content script (де воно вже в пам'яті), а очікування обробки й
генерацію конспекту веде service worker фоново через `chrome.alarms`, тож вкладку Meet можна
закрити одразу. Низька роздільність кадрів тримає запит у межах лімітів навіть для довгих
зустрічей.
Comment thread content.js
Comment on lines +280 to 288
async function initRecovery() {
try {
await RecStore.pruneOld();
const orphans = await RecStore.listOrphans();
if (orphans.length && !isRecording) showRecoveryBanner(orphans[0]);
} catch (e) {
console.warn('[MeetRec] recovery init:', e);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Конкурентний конфлікт відновлення між вкладками

listOrphans() повертає всі сесії з IndexedDB без фільтрації за станом. Якщо користувач відкриє дві вкладки Google Meet (одна веде запис, друга — відкрита щойно), initRecovery в другій вкладці побачить активну сесію першої вкладки як "осиротілу" і показуватиме банер відновлення. При натисканні «✕» → dismissRecovery → RecStore.deleteSession(session.id) — дані активного запису в IndexedDB видаляються, і перша вкладка вже не зможе відновити запис після аварійного завершення.

Щоб усунути проблему, потрібно або зберігати tabId у метаданих сесії і порівнювати з chrome.tabs через background, або додати поле heartbeat (оновлюється кожні N секунд під час запису) і при initRecovery перевіряти, чи не оновлювався heartbeat нещодавно.

Fix in Cursor

Comment thread content.js
Comment on lines +341 to +344
// Аудіо-доріжку з журналу шлемо в Gemini; якщо її нема (старі сесії) — фолбек на відео.
const audioBlob = await RecStore.readBlob(session.id, 'audio/webm', 'audio').catch(() => null);
await RecStore.deleteSession(session.id);
await maybeGemini(audioBlob && audioBlob.size ? audioBlob : blob, name, saved.folderId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Неправильний MIME-тип при відновленні: відео-blob передається в Gemini як audio/webm

У рядку 344 функція recover передає резервний відео-blob у maybeGemini, коли аудіо-доріжка відсутня (старі сесії без окремого треку). Але maybeGemini безумовно викликає Gemini.geminiUploadFile(audioBlob, geminiApiKey, 'audio/webm') — тобто відео-контент маркується заголовком X-Goog-Upload-Header-Content-Type: audio/webm. Gemini Files API відхилить або неправильно оброблятиме відео-файл з таким MIME-типом, і конспект або не буде згенерований, або генерація поверне помилку.

Fix in Cursor

Comment thread gdrive.js
Comment on lines +15 to +16
const safe = name.replace(/'/g, "\\'");
let q = `mimeType='application/vnd.google-apps.folder' and name='${safe}' and trashed=false`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Неповна екранізація символів у Drive API-запиті

Поточна санітизація name.replace(/'/g, "\\'") обробляє лише одинарні лапки, але не зворотні слеші. Якщо в назві папки з'явиться послідовність \', подвоєний слеш може залишити незакриту цитату в рядку Drive API-запиту. Безпечніший варіант — екранувати обидва символи: спочатку \\, потім '.

Fix in Cursor

Comment thread content.js
Comment on lines +232 to +236
} catch (driveErr) {
console.warn('[MeetRec] Drive недоступний, зберігаю локально:', driveErr);
downloadLocally(blob, name);
return { where: 'local', folderId: null };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Помилки downloadLocally мовчки ковтаються

downloadLocally(blob, name) викликається синхронно без await і без обгортки try/catch. Якщо програмний клік <a download> заблокований, користувач отримає статус «Готово ✓ — збережено локально» без реального збереження файлу і без жодного сигналу про помилку.

Fix in Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants