diff --git a/src/background/index.ts b/src/background/index.ts index dfac677..c069243 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -98,6 +98,17 @@ const NOTIFICATION_STARTUP_PREFIX = 'mokosh-startup-'; const NOTIFICATION_RECOVERY_PREFIX = 'mokosh-recovery-'; const POPUP_HTML_PATH = 'src/popup/index.html'; +// ─── Plan 01-10 onboarding constants (D-17-onboarding) ─────────────── +// Project naming standard: SCREAMING_SNAKE for true constants. These +// drive the first-install welcome-tab flow (chrome.runtime.onInstalled +// → openWelcomeIfFirstInstall; flag persisted via chrome.storage.local). +// The 'onboarding-completed' key is the storage-schema identifier +// preserved across SW respawns (and surveyed by the unit test in +// tests/background/onboarding.test.ts Test A). +const ONBOARDING_FLAG = 'onboarding-completed'; +const ONBOARDING_INSTALLED_AT = 'installed-at'; +const WELCOME_PATH = 'src/welcome/welcome.html'; + // Plan 01-12 Wave 4: operator-facing copy fallbacks for the notification // title (extName) + the two notification messages. Same `|| fallback` // pattern as the popup — unit-test contexts without chrome.i18n stub @@ -217,6 +228,58 @@ async function ensureOffscreen() { } } +/** + * Open the welcome page on first install (Plan 01-10 D-17-onboarding). + * + * The 'D-17-onboarding' suffix disambiguates from D-17-port-lifecycle + * per CONTEXT.md lines 540-545. Trigger conditions (all must hold): + * - details.reason === 'install' (NOT 'update' / 'chrome_update' / + * 'shared_module_update'); + * - chrome.storage.local key 'onboarding-completed' NOT === true. + * + * Side effects on first install: + * - chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')}) + * - chrome.storage.local.set({'onboarding-completed': true, + * 'installed-at': Date.now()}) + * + * Failure mode: any thrown chrome.* call is caught + logged via + * logger.warn. The welcome tab failing is NOT fatal — the toolbar + * onClicked path (D-16-toolbar) remains the operator's start path and + * is independent of the onboarding flow. + * + * Architectural note: the fetch of 'onboarding-completed' uses the + * EXACT string key (no array form) so the unit-test contract in + * tests/background/onboarding.test.ts Test A's assertion + * "saw chrome.storage.local.get('onboarding-completed')" holds. The + * storage-schema cross-version-compat pin (I-02 lesson preserved from + * prior plan draft) lives in that test. + */ +async function openWelcomeIfFirstInstall( + details: chrome.runtime.InstalledDetails, +): Promise { + 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, + [ONBOARDING_INSTALLED_AT]: Date.now(), + }); + logger.log( + 'Welcome tab opened (D-17-onboarding); onboarding flag set.', + ); + } catch (err) { + logger.warn('openWelcomeIfFirstInstall failed:', err); + } +} + // Outer-bound buffer fetch budget. Larger than the legacy // BUFFER_FETCH_TIMEOUT_MS (was 2 s; per-port-attempt) because the new // architecture covers MULTIPLE port-replacement retries inside one outer @@ -1002,6 +1065,14 @@ chrome.runtime.onInstalled.addListener((details) => { logger.warn('IDB cleanup failed:', e); } initialize(); + // Plan 01-10 D-17-onboarding: open welcome tab on first install. + // Fire-and-forget — the helper logs its own errors and rejected + // promises are caught at the .catch boundary so they cannot escape + // the synchronous listener. The toolbar onClicked start path + // (D-16-toolbar) is independent of this flow. + openWelcomeIfFirstInstall(details).catch((err) => { + logger.warn('openWelcomeIfFirstInstall threw:', err); + }); }); // Запуск при старте Service Worker