Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-10-PLAN.md
Mark 2e499d7387 docs(01): add Plans 01-08 / 01-09 / 01-10 (amended Phase 1 charter)
Plans cover the post-D-13 architecture and the auto-start UX charter
expansion that landed during 2026-05-16 UAT:

- Plan 01-08 — WebM remux via ts-ebml@3.0.2 + webm-muxer@5.1.4. Replaces
  the broken file-concat in mergeVideoSegments with a real single-EBML
  remux. Drives the 2 RED tests in tests/offscreen/webm-playback.test.ts
  to GREEN. Regenerates the canonical fixture against the remuxer.
  5 tasks (4 TDD + 1 operator empirical checkpoint), wave 1.

- Plan 01-09 — Whole-desktop constraint (displaySurface:'monitor',
  cursor:'always') + post-grant validation, chrome.action.onClicked
  direct toolbar invocation, chrome.action badge state machine
  (REC/OFF/ERROR), chrome.runtime.onStartup notification + recovery
  notification on onUserStoppedSharing, popup scoped to SAVE-only.
  17 new test assertions across 4 test files. smoke.sh updated to
  auto-select an entire screen. 5 tasks (4 TDD + 1 operator empirical
  checkpoint), wave 2 (depends on 01-08).

- Plan 01-10 — chrome.runtime.onInstalled welcome tab on first install
  via chrome.storage.local guard; vanilla welcome.html/ts/css bundle
  with single "Начать запись" button consuming install-time activation.
  Uses centralized Logger pattern. 4 tasks (3 TDD + 1 operator empirical
  checkpoint), wave 3 (depends on 01-09).

CONTEXT.md amendment block appended with 4 disambiguated decisions:
- D-14-remux: WebM remux supersedes D-13 file-concat
- D-15-display-surface: whole-desktop + cursor visibility lifted from
  Phase 5 deferral
- D-16-toolbar: toolbar onClicked + popup SAVE-only + badge state
  machine + onStartup/recovery notifications
- D-17-onboarding: welcome tab on first install (distinct from
  D-17-port-lifecycle from Option C)
The earlier D-17 port-lifecycle heading also renamed to hyphenated
form for cross-ref consistency.

Plan-check loop: 3 iterations (initial + 2 revisions). Iteration 1
surfaced 11 findings (2 BLOCKER + 6 WARNING + 3 INFO); all addressed
via revision iter 1 with checker-recommended fixes. Iteration 2
surfaced 3 derivative regressions (literal-string grep anchors from
the iter-1 fixes did not match live CONTEXT.md); all addressed in iter
2 with empirically-validated literals. Iteration 3 PASSED clean.

Validation: gsd-sdk frontmatter.validate + verify.plan-structure both
return valid=True for all 3 plans. Plan 01-08 Task 4 verify-chain
grep tested end-to-end against live CONTEXT.md (exit 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:19:22 +02:00

551 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 01-stabilize-video-pipeline
plan: 10
type: execute
wave: 3
depends_on:
- 01-09
files_modified:
- manifest.json
- src/welcome/welcome.html
- src/welcome/welcome.ts
- src/welcome/welcome.css
- src/background/index.ts
- tests/background/onboarding.test.ts
autonomous: false
requirements:
- REQ-video-ring-buffer
tags:
- onboarding
- welcome
- chrome.runtime.onInstalled
- chrome.storage
- web_accessible_resources
must_haves:
truths:
- "On first install (chrome.runtime.onInstalled with reason 'install') a welcome tab opens automatically pointing at src/welcome/welcome.html (resolved via chrome.runtime.getURL)."
- "On subsequent installs (reason 'update' or 'chrome_update') OR on a second-install when chrome.storage.local already has onboarding-completed flag, NO welcome tab opens (assert via tests/background/onboarding.test.ts: chrome.tabs.create is called exactly once across two install events)."
- "Welcome page has a single 'Start Mokosh' button that, on click, sends a REQUEST_PERMISSIONS message to the SW which kicks off the existing startVideoCapture flow (consuming the click as user gesture for getDisplayMedia)."
- "After successful start the welcome page updates its DOM to show a 'Recording started — close this tab when ready' confirmation state (no full page reload; uses chrome.runtime.onMessage to listen for the RECORDING_STARTED ack OR observes the popup-toolbar badge transition by re-checking chrome.action.getBadgeText)."
- "manifest.json adds 'storage' to permissions (if not already present — it IS already present per current manifest, so just verify; do not duplicate) AND adds web_accessible_resources entry for src/welcome/welcome.html so chrome.tabs.create with chrome.runtime.getURL resolves."
- "Onboarding test covers BOTH first-install AND subsequent-install paths via two synthesized onInstalled events with different reason fields."
artifacts:
- path: "src/welcome/welcome.html"
provides: "Welcome page markup with a single Start button, brief explainer paragraph, no fancy framework (vanilla DOM per project style)."
min_lines: 25
- path: "src/welcome/welcome.ts"
provides: "Click-handler that sends REQUEST_PERMISSIONS to SW; transitions DOM to confirmation state on success; surfaces error on failure."
min_lines: 30
- path: "src/welcome/welcome.css"
provides: "Minimal styling (~30 LOC) consistent with the project's existing popup style.css aesthetic."
min_lines: 20
- path: "src/background/index.ts"
provides: "onInstalled handler extended: if reason === 'install' AND chrome.storage.local does not have 'onboarding-completed' flag, open welcome tab via chrome.tabs.create + set flag. Existing IDB cleanup + initialize() call preserved."
contains: "src/welcome/welcome.html"
- path: "manifest.json"
provides: "web_accessible_resources array with entry for src/welcome/welcome.html. 'storage' permission verified present."
contains: "web_accessible_resources"
- path: "tests/background/onboarding.test.ts"
provides: "RED-to-GREEN tests pinning: (a) first install creates welcome tab; (b) update/chrome_update does NOT create welcome tab; (c) repeated install when flag already set does NOT create welcome tab."
key_links:
- from: "src/background/index.ts onInstalled handler"
to: "chrome.tabs.create"
via: "chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')})"
pattern: "chrome.tabs.create"
- from: "src/welcome/welcome.ts click handler"
to: "chrome.runtime.sendMessage"
via: "{type: 'REQUEST_PERMISSIONS'} — kicks the existing SW startVideoCapture flow"
pattern: "REQUEST_PERMISSIONS"
- from: "manifest.json web_accessible_resources"
to: "src/welcome/welcome.html"
via: "resources array entry making the page extension-accessible"
pattern: "src/welcome/welcome.html"
---
<objective>
First-install operator-friendly activation. When the extension is installed (typically from the Chrome Web Store one day, but also from a local Load Unpacked), a welcome tab opens automatically with a brief explainer + a single 'Start Mokosh' button. Operator reads the explainer + clicks → recording starts immediately (consuming the install-time click as the getDisplayMedia user gesture).
This complements Plan 01-09's toolbar-onClicked + onStartup notification flow:
- Plan 01-09 makes Chrome-startup-time activation easy (notification → click → picker).
- Plan 01-10 makes install-time activation easy (welcome tab → click → picker).
- Both consume the same RECORDING_ERROR/RECORDING_STARTED surface; both reuse the existing src/background/index.ts startVideoCapture path.
Output:
- src/welcome/welcome.{html,ts,css} — 3 new files for the onboarding page (vanilla DOM per project style; the existing popup at src/popup/index.html is the analog).
- src/background/index.ts — onInstalled handler extended with first-install detection (chrome.storage.local flag) + chrome.tabs.create call.
- manifest.json — adds web_accessible_resources entry pointing at the welcome page so chrome.tabs.create with chrome.runtime.getURL resolves. (storage permission already present per current manifest; verify it's not removed.)
- tests/background/onboarding.test.ts — pins the first-install vs subsequent-install vs already-completed contracts.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
@.planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md
@.planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md
@src/background/index.ts
@src/popup/index.html
@src/popup/index.ts
@src/popup/style.css
@manifest.json
<interfaces>
Key Chrome API surfaces.
chrome.runtime.onInstalled.addListener fires with {reason: 'install' | 'update' | 'chrome_update' | 'shared_module_update'}. The handler in src/background/index.ts line 724 already exists for IDB cleanup; this plan extends it.
chrome.storage.local.get + .set pattern:
```
const { 'onboarding-completed': onboardingCompleted } = await chrome.storage.local.get('onboarding-completed');
if (details.reason === 'install' && onboardingCompleted !== true) {
await chrome.tabs.create({ url: chrome.runtime.getURL('src/welcome/welcome.html') });
await chrome.storage.local.set({ 'onboarding-completed': true });
}
```
chrome.tabs.create signature:
```
chrome.tabs.create({ url: string, active?: boolean }): Promise<chrome.tabs.Tab>
```
manifest.json web_accessible_resources shape (MV3):
```
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
]
```
Without this entry, chrome.runtime.getURL returns a path but loading it via chrome.tabs.create fails with a permission error in MV3.
Welcome page structure follows the existing popup pattern (src/popup/index.html + src/popup/style.css):
- HTML: container div, h1, p explainer, button. Russian copy consistent with popup ('Сохранить отчёт об ошибке' precedent).
- TS: vanilla DOM (no framework); attach addEventListener('click', handler) to the button; call chrome.runtime.sendMessage({type: 'REQUEST_PERMISSIONS'}); on success update the DOM to show confirmation; on failure show error.
- CSS: minimal, consistent palette with popup's style.css.
The existing src/background/index.ts onInstalled handler at line 724:
```
chrome.runtime.onInstalled.addListener((details) => {
logger.log('Extension installed/updated:', details.reason);
try {
indexedDB.deleteDatabase('VideoRecorderDB');
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
});
```
This plan extends it WITHOUT touching the IDB cleanup or initialize() call — adds the onboarding check as an additional step (sequentially after the existing logic).
Note: the existing onInstalled handler is SYNCHRONOUS (no async/await). Wrapping the new onboarding logic in a self-executing async IIFE preserves the handler's sync signature while allowing await for chrome.storage.local + chrome.tabs.create.
Vite/crxjs note: src/welcome/welcome.html needs to be declared as a rollupOptions.input entry in vite.config.ts so the build bundles it into dist/. The existing vite.config.ts already has src/offscreen/index.html as an input; add src/welcome/welcome.html alongside. Otherwise the welcome page won't appear in dist/.
Existing tests/background/request-id-protocol.test.ts chrome stub pattern includes chrome.runtime.onInstalled.addListener as a vi.fn(); extend with chrome.tabs.create + chrome.storage.local.{get, set} for this test file.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: RED tests — onInstalled first-install creates welcome tab; subsequent-install does not; already-completed flag suppresses.</name>
<read_first>
- tests/background/request-id-protocol.test.ts lines 53-200 (chrome stub scaffold)
- src/background/index.ts lines 724-737 (existing onInstalled handler)
- manifest.json (current shape — confirms storage permission already present)
</read_first>
<files>tests/background/onboarding.test.ts</files>
<behavior>
Three RED tests:
- Test A: when onInstalled fires with reason 'install' AND chrome.storage.local has no 'onboarding-completed' flag, chrome.tabs.create is called exactly once with a URL containing 'src/welcome/welcome.html'. After the call, chrome.storage.local.set is called with `{'onboarding-completed': true}`. **I-02 fix (2026-05-16 checker pass):** additionally assert `expect(chromeStub.storage.local.get).toHaveBeenCalledWith('onboarding-completed')` — this pins the EXACT key name the handler reads from storage. Without this assertion, a refactor that renamed the constant to e.g. `'mokosh-onboarding-done'` would still pass Test A's set-side assertion (because both the read and write would use the new name in lock-step) while silently breaking compatibility with any installed user's storage from the prior schema. One additional line of test code; catches a real cross-version compatibility bug class.
- Test B: when onInstalled fires with reason 'update', chrome.tabs.create is NOT called.
- Test C: when onInstalled fires with reason 'install' BUT chrome.storage.local already has 'onboarding-completed': true, chrome.tabs.create is NOT called.
</behavior>
<action>
1. Create tests/background/onboarding.test.ts.
2. Copy the chrome stub from tests/background/request-id-protocol.test.ts. Extend with:
- chrome.tabs: { create: vi.fn().mockResolvedValue({ id: 99, url: 'chrome-extension://...' }) }
- chrome.storage: { local: { get: vi.fn(), set: vi.fn().mockResolvedValue(undefined) } }
- chrome.runtime.onInstalled: { addListener: vi.fn(), _callbacks: [] } — and have addListener push to _callbacks for test-driven invocation
- chrome.runtime.getURL: (path) => 'chrome-extension://test-id/' + path
3. Test A:
- chromeStub.storage.local.get.mockResolvedValue({}); // empty — no flag set
- vi.resetModules(); set globalThis.chrome; await import('../../src/background/index');
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'install' });
- Drain microtasks via await Promise.resolve a few times.
- Assert chrome.tabs.create called exactly once with object whose url contains 'src/welcome/welcome.html'.
- Assert chrome.storage.local.set called with {'onboarding-completed': true}.
- **I-02 fix:** assert `expect(chromeStub.storage.local.get).toHaveBeenCalledWith('onboarding-completed')` — pins the exact storage key the handler reads. (One line of test code beyond the existing set-side check.)
4. Test B:
- chromeStub.storage.local.get.mockResolvedValue({});
- vi.resetModules(); set globalThis.chrome; await import.
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'update' });
- Drain microtasks.
- Assert chrome.tabs.create NOT called.
- Assert chrome.storage.local.set NOT called.
5. Test C:
- chromeStub.storage.local.get.mockResolvedValue({ 'onboarding-completed': true });
- vi.resetModules(); set globalThis.chrome; await import.
- Synthesize: await chromeStub.runtime.onInstalled._callbacks[0]({ reason: 'install' });
- Drain microtasks.
- Assert chrome.tabs.create NOT called.
6. Run npx vitest run tests/background/onboarding.test.ts — all 3 must be RED today (the new onInstalled extension does not exist yet).
7. DO NOT modify src/background/index.ts in this task.
</action>
<verify>
<automated>npx vitest run tests/background/onboarding.test.ts</automated>
</verify>
<acceptance_criteria>
- tests/background/onboarding.test.ts exists with 3 tests; all 3 RED.
- Baseline (Plan 01-09 final: 16 files / 76 tests / all GREEN) + 3 new RED here = 17 files / 79 tests / 3 failed | 76 passed.
- npx tsc --noEmit exit 0.
</acceptance_criteria>
<done>3 RED tests pin the onboarding routing contract; baseline preserved.</done>
</task>
<task type="auto">
<name>Task 2: Create welcome page assets src/welcome/welcome.{html,ts,css}; register vite entry; add web_accessible_resources to manifest.</name>
<read_first>
- src/popup/index.html (style analog)
- src/popup/style.css (palette + sizing reference)
- src/popup/index.ts (vanilla DOM + chrome.runtime.sendMessage pattern)
- vite.config.ts (where to add rollupOptions.input entry)
- manifest.json (current web_accessible_resources is ABSENT — this task adds it)
</read_first>
<files>src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.css, vite.config.ts, manifest.json</files>
<action>
1. Create src/welcome/welcome.html (Russian per project provenance; matches popup language):
```
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Добро пожаловать в Mokosh</title>
<link rel="stylesheet" href="welcome.css">
</head>
<body>
<main class="welcome">
<h1>Добро пожаловать в Mokosh</h1>
<p>
Расширение записывает последние 30 секунд экрана и 10 минут логов
вашего браузера, чтобы при возникновении бага вы могли одним
кликом сохранить полный отчёт для службы поддержки.
</p>
<p>
Нажмите кнопку ниже, чтобы выбрать экран и начать запись.
Запись идёт фоном; ваши данные не отправляются никуда —
только сохраняются локально по вашему запросу.
</p>
<button id="startButton" class="start-button" type="button">
Начать запись
</button>
<p id="statusMessage" class="status-message"></p>
</main>
<script type="module" src="welcome.ts"></script>
</body>
</html>
```
2. Create src/welcome/welcome.ts (vanilla DOM; absolute imports per project style). **W-06 fix (2026-05-16 checker pass): use the centralized `Logger` class from `src/shared/logger.ts` instead of an inline `console.log` wrapper.** This matches the background + offscreen + popup convention (popup currently has a bespoke inline `function log` per `src/popup/index.ts:18` — that's a known stylistic divergence the popup hasn't yet been refactored against; the welcome page lands clean from day one). The `Logger` class is the SW logger by published shape (prefix `[SW:<context>]`) — the welcome page is NOT an SW, so the SW-prefix is semantically loose; if the executor finds this jarring during implementation, they MAY add a new `WelcomeLogger` class to `src/shared/logger.ts` mirroring the `OffscreenLogger`/`ContentLogger` pattern (prefix `[WC:<context>]`) and use that here instead. EITHER choice satisfies W-06's "use the centralized logger" requirement; the inline `function log` form is what the checker rejected.
```
// src/welcome/welcome.ts — onboarding click-handler (Plan 01-10 D-17-onboarding).
//
// Sends REQUEST_PERMISSIONS to the SW which routes through the same
// startVideoCapture path as the toolbar onClicked handler (Plan 01-09
// D-16-toolbar). The button click counts as the user gesture for
// getDisplayMedia.
// W-06 fix: use the centralized Logger from src/shared/logger.ts
// instead of an inline console.log wrapper. The Logger class emits
// `[SW:<context>] <ISO timestamp>` lines; the prefix is semantically
// loose for a welcome page (not an SW) but matches the background +
// offscreen logger discipline. If the executor opts to add a new
// WelcomeLogger class to src/shared/logger.ts (prefix `[WC:Welcome]`)
// and import that instead, that ALSO satisfies W-06.
import { Logger } from '../shared/logger';
const logger = new Logger('Welcome');
const startButton = document.getElementById('startButton') as HTMLButtonElement | null;
const statusMessage = document.getElementById('statusMessage') as HTMLParagraphElement | null;
async function onStart(): Promise<void> {
if (startButton === null || statusMessage === null) {
return;
}
startButton.disabled = true;
statusMessage.textContent = 'Открываем выбор источника...';
statusMessage.className = 'status-message';
try {
const response = await chrome.runtime.sendMessage({
type: 'REQUEST_PERMISSIONS',
});
logger.log('REQUEST_PERMISSIONS response:', response);
if (response?.granted === true) {
statusMessage.textContent = 'Запись начата. Эту вкладку можно закрыть.';
statusMessage.className = 'status-message success';
startButton.textContent = 'Запись активна';
} else {
startButton.disabled = false;
statusMessage.textContent = 'Не удалось начать запись. Попробуйте снова.';
statusMessage.className = 'status-message error';
}
} catch (err) {
logger.warn('Start failed:', err);
startButton.disabled = false;
statusMessage.textContent = 'Ошибка: ' + ((err as Error)?.message ?? String(err));
statusMessage.className = 'status-message error';
}
}
function init(): void {
if (startButton !== null) {
startButton.addEventListener('click', onStart);
}
}
document.addEventListener('DOMContentLoaded', init);
```
3. Create src/welcome/welcome.css (consistent palette with src/popup/style.css):
```
html, body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
color: #1f1f1f;
}
.welcome {
max-width: 600px;
margin: 60px auto;
padding: 32px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.welcome h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
}
.welcome p {
line-height: 1.5;
margin: 0 0 16px;
}
.start-button {
display: block;
width: 100%;
padding: 12px 16px;
margin: 24px 0 16px;
font-size: 16px;
font-weight: 500;
background: #00C853;
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
}
.start-button:hover:not(:disabled) {
background: #00B248;
}
.start-button:disabled {
background: #BDBDBD;
cursor: not-allowed;
}
.status-message {
min-height: 1.4em;
font-size: 14px;
color: #616161;
}
.status-message.success {
color: #00C853;
}
.status-message.error {
color: #D32F2F;
}
```
4. Update vite.config.ts — add 'src/welcome/welcome.html' to rollupOptions.input alongside the existing offscreen entry:
```
rollupOptions: {
input: {
offscreen: 'src/offscreen/index.html',
welcome: 'src/welcome/welcome.html',
},
},
```
5. Update manifest.json — add a web_accessible_resources array:
```
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
]
```
Insert this after the "host_permissions" block. Confirm 'storage' is already in permissions (it IS per current manifest line 11; do not duplicate).
6. Run npm run build — exit 0; confirm dist/src/welcome/welcome.html exists and the new web_accessible_resources entry is in dist/manifest.json.
7. Run npx tsc --noEmit — exit 0.
8. Run npx vitest run — baseline preserved (17 files / 76 GREEN + 3 RED from Task 1; the 3 RED stay RED).
</action>
<verify>
<automated>npm run build && npx tsc --noEmit && test -f dist/src/welcome/welcome.html && grep -q web_accessible_resources dist/manifest.json</automated>
</verify>
<acceptance_criteria>
- src/welcome/welcome.{html,ts,css} exist with the contents above (verbatim or stylistically equivalent).
- vite.config.ts has the welcome input entry.
- manifest.json has the web_accessible_resources block with welcome.html.
- npm run build exit 0; dist/ contains src/welcome/welcome.html and a manifest with web_accessible_resources.
- npx tsc --noEmit exit 0.
- Baseline preserved (17 files / 79 tests / 3 RED from Task 1 still RED; 76 GREEN).
</acceptance_criteria>
<done>Welcome page assets staged + build pipeline picks them up + manifest declares them accessible; ready for the SW handler in Task 3.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: GREEN — extend onInstalled in src/background/index.ts with first-install welcome-tab logic; drives Task 1 tests to GREEN.</name>
<read_first>
- tests/background/onboarding.test.ts (contracts from Task 1)
- src/background/index.ts lines 724-737 (existing onInstalled handler)
</read_first>
<files>src/background/index.ts</files>
<action>
1. Define a constant near the top of the file (alongside other top-level constants like VIDEO_MIME_FALLBACK):
const ONBOARDING_FLAG = 'onboarding-completed';
const WELCOME_PATH = 'src/welcome/welcome.html';
2. Extract a helper function (placed near the other helpers, e.g. just below ensureOffscreen at line 86):
async function openWelcomeIfFirstInstall(details: chrome.runtime.InstalledDetails): Promise<void> {
if (details.reason !== 'install') {
return;
}
try {
const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
if (stored[ONBOARDING_FLAG] === true) {
logger.log('Onboarding already completed; skipping welcome tab.');
return;
}
const url = chrome.runtime.getURL(WELCOME_PATH);
await chrome.tabs.create({ url });
await chrome.storage.local.set({ [ONBOARDING_FLAG]: true });
logger.log('Welcome tab opened and onboarding flag set.');
} catch (err) {
logger.warn('openWelcomeIfFirstInstall failed:', err);
}
}
Document with a JSDoc header per project style; cite Plan 01-10 **D-17-onboarding** (this plan's CONTEXT amendment marker — appended at plan-creation time alongside D-14-remux / D-15-display-surface / D-16-toolbar). **B-02 fix (2026-05-16 checker pass):** the original draft of this line cited bare 'D-16' which is ambiguous — the historical decisions block (CONTEXT.md line 100) has `**D-16:** Video buffer ownership moves to the offscreen document`, completely unrelated to the toolbar UX. To disambiguate per the D-17-port-lifecycle / D-17-onboarding precedent, all amendment-block markers carry a `-suffix`: D-14-remux (remux helper), D-15-display-surface (whole-desktop constraint), D-16-toolbar (toolbar+badge+notifications), D-17-onboarding (this plan's welcome-tab). Citing D-17-onboarding here points the JSDoc reader at the right amendment block on CONTEXT.md.
3. Modify the existing onInstalled handler (line 724) to invoke the helper. The existing handler is synchronous; wrap the new call in a fire-and-forget pattern OR convert the listener to async — both are valid for chrome.runtime.onInstalled (Chrome 91+ accepts async listeners; the IDB cleanup is sync and stays at the top, and the welcome flow is async at the bottom):
chrome.runtime.onInstalled.addListener((details) => {
logger.log('Extension installed/updated:', details.reason);
try {
indexedDB.deleteDatabase('VideoRecorderDB');
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
// Plan 01-10: open welcome tab on first install. Fire-and-forget;
// the helper logs its own errors.
openWelcomeIfFirstInstall(details).catch((err) => {
logger.warn('openWelcomeIfFirstInstall threw:', err);
});
});
4. Run npx vitest run tests/background/onboarding.test.ts — all 3 must flip GREEN.
5. Run full suite — 17 files / 79 tests / all GREEN.
6. Run npx tsc --noEmit — exit 0.
7. Run npm run build — exit 0.
Naming/style: ONBOARDING_FLAG + WELCOME_PATH SCREAMING_SNAKE per project rule for true constants. openWelcomeIfFirstInstall — full-word camelCase. No 'continue'; if-else chains. No 'as any'. The chrome.storage.local.get key-name access is type-safe via dynamic indexing (acceptable per @types/chrome's chrome.storage.local signature: returns Record<string, unknown>).
</action>
<verify>
<automated>npx vitest run tests/background/onboarding.test.ts</automated>
</verify>
<acceptance_criteria>
- openWelcomeIfFirstInstall helper exists in src/background/index.ts with the documented behavior.
- onInstalled handler invokes the helper.
- All 3 onboarding tests GREEN.
- Full suite 17 files / 79 tests / all GREEN.
- npx tsc --noEmit exit 0.
- npm run build exit 0.
</acceptance_criteria>
<done>onInstalled extended; tests GREEN; welcome flow wired end-to-end at the SW layer.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 4: Operator empirical check — first install opens welcome tab; click triggers picker; recording starts; tests confirm second install does NOT re-open welcome.</name>
<files>(operator-driven; no specific source file modified by this checkpoint)</files>
<action>See <how-to-verify> below — operator-driven empirical check; the executor agent must not bypass this checkpoint by stubbing.</action>
<verify>
<automated>echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate"</automated>
</verify>
<done>Operator types "approved" after running the how-to-verify steps. See <resume-signal> for the exact gate.</done>
<what-built>
Tasks 1-3 landed: src/welcome/* page bundle, manifest web_accessible_resources, onInstalled handler extended with chrome.storage.local-gated welcome-tab opening. The 3 unit tests are GREEN. This checkpoint validates real Chrome behavior end-to-end.
</what-built>
<how-to-verify>
1. Build: npm run build (exit 0).
2. Wipe smoke profile: rm -rf /tmp/mokosh-smoke-profile (or KEEP_PROFILE=0 ./smoke.sh which does the wipe).
3. Run smoke: KEEP_PROFILE=0 ./smoke.sh. Chrome launches with fresh profile.
4. Load Unpacked → select dist/. THE WELCOME TAB SHOULD AUTOMATICALLY OPEN within ~1 second after the extension loads. The tab URL should look like chrome-extension://<id>/src/welcome/welcome.html.
5. Confirm the welcome page renders: title 'Добро пожаловать в Mokosh', explainer paragraphs, big green 'Начать запись' button, empty status message line.
6. Click 'Начать запись'. The button disables; status message shows 'Открываем выбор источника...'; Chrome's screen-share picker appears (monitor-only per Plan 01-09).
7. Pick 'Entire screen' and accept. Status message transitions to 'Запись начата. Эту вкладку можно закрыть.' The toolbar badge transitions to REC (green) per Plan 01-09.
8. Close the welcome tab.
9. Now reload the extension at chrome://extensions (toggle off then on). Observe: the welcome tab does NOT open this time (because chrome.storage.local has 'onboarding-completed' === true from the first install). Only the existing toolbar/badge behavior applies.
10. To re-validate the onInstalled='install' path: wipe the profile (Cmd+Q Chrome → rm -rf /tmp/mokosh-smoke-profile → relaunch smoke.sh → Load Unpacked again). Welcome tab opens again because storage.local was wiped with the profile.
11. If step 4 (welcome tab opens), step 6 (picker appears on click), step 7 (recording starts), or step 9 (re-load does NOT re-open) fails: document the failure mode + Chrome version + the SW console errors. Iterate on Task 2 (asset bundling) or Task 3 (SW handler) accordingly.
</how-to-verify>
<resume-signal>
Type 'approved' after steps 4, 6, 7, 9 all PASS. If any step fails, paste the failure diagnostic.
</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| welcome page <-> SW | Welcome page (a same-origin extension page) sends REQUEST_PERMISSIONS via chrome.runtime.sendMessage. T-1-NEW-05-01 sender-id check in the SW's onMessage listener (line 635) already validates sender.id === chrome.runtime.id; no new boundary. |
| chrome.storage.local <-> SW | Storage flag is non-secret (boolean true); even if leaked, the only effect is suppressing the welcome tab on future installs. No PII; no sensitive content. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-10-01 | Tampering | adversary clears onboarding-completed flag to spam welcome tabs | accept | Welcome tab is non-destructive; the worst case is an extra tab on each install which the operator can close. No data exfiltration path. |
| T-1-10-02 | Information Disclosure | welcome.html leaking via web_accessible_resources fingerprinting (extensions can be enumerated by sites probing chrome-extension:// URLs in web_accessible_resources) | accept | Extension identifier is already discoverable through chrome.runtime.getURL exposure on any extension page; matches:[\"<all_urls>\"] is the standard pattern for welcome flows. Phase 5 hardening could narrow matches to a specific install-confirmation domain if needed; out of scope. |
| T-1-10-03 | Denial of Service | adversary controls chrome.storage.local quota via web requests | mitigate | Single boolean flag uses ~50 bytes; storage.local quota is 10 MB; not exploitable. |
| T-1-10-04 | Elevation of Privilege | welcome page tricks SW into bypassing checkpoints | mitigate | Welcome page sends only REQUEST_PERMISSIONS — same route as the popup. The SW's existing sender-id check + the chrome.action user-gesture model both still apply. No new elevation path. |
</threat_model>
<verification>
- npx vitest run shows 17 files / 79 tests / all GREEN.
- npx tsc --noEmit exit 0.
- npm run build exit 0; dist/src/welcome/welcome.html exists; dist/manifest.json contains web_accessible_resources with welcome.html.
- Operator empirical: first install opens welcome tab; click triggers picker; recording starts; reload does NOT re-open welcome.
- grep -n "chrome.tabs.create" src/background/index.ts returns at least one match.
- grep -n "welcome.html" manifest.json returns at least one match.
- grep -n "openWelcomeIfFirstInstall" src/background/index.ts returns at least one match.
</verification>
<success_criteria>
Plan 01-10 is complete when:
1. The 3 onboarding tests are GREEN.
2. All 76 baseline GREEN tests from Plan 01-09 remain GREEN.
3. Operator runs the Task 4 checkpoint and confirms first install opens welcome tab, click starts recording, reload does NOT re-open.
4. tsc + build clean; manifest + vite + welcome assets all consistent.
</success_criteria>
<output>
After completion, create .planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md per the standard template. Cite: the 3 new tests landed GREEN; new src/welcome/ page bundle; manifest web_accessible_resources delta; onInstalled extension; first-install vs subsequent-install behavior confirmed by Task 4 operator check.
</output>