diff --git a/entrypoints/popup/main.ts b/entrypoints/popup/main.ts index 4624dfb..7c13575 100644 --- a/entrypoints/popup/main.ts +++ b/entrypoints/popup/main.ts @@ -34,7 +34,7 @@ const root = app; let tabHostname = ''; let activeTabId: number | null = null; let settings: ExtensionSettings = { ...DEFAULT_SETTINGS }; -let currentView: 'main' | 'settings' = 'main'; +let currentView: 'welcome' | 'main' | 'settings' = 'main'; let lastMode: CheckMode = 'apex'; let lastResult: CheckResult | null = null; @@ -354,15 +354,18 @@ function escapeHtml(s: string): string { .replace(/`/g, '`'); } -function modeChips(mode: CheckMode): string { +function modeChips(mode: CheckMode, showExact: boolean): string { + const exactChip = showExact + ? `` + : ''; return `
- + ${exactChip}
`; } @@ -384,14 +387,46 @@ function renderHeaderBrand(hostname: string): string { `; } -function renderLoading(mode: CheckMode): void { - const headerHost = tabHostname - ? resolveCheckTargets(tabHostname, mode).queryHost +function renderWelcome(): void { + const targets = resolveCheckTargets(tabHostname, 'apex'); + const rootHost = targets.queryHost; + const tabDiffers = rootHost !== targets.tab; + const tabLine = tabDiffers + ? `

Use Tab hostname only when you want to test the exact subdomain shown in this tab: ${escapeHtml(tabHostname)}.

` + : ''; + const tabButton = tabDiffers + ? '' : ''; + + root.innerHTML = shellWithFabFooterOnly(` +
+ ${renderHeaderBrand(rootHost)} +
+
+

First run

+

JayQuery starts at the root domain.

+

Most email security records are set on the main domain, so JayQuery checks ${escapeHtml(rootHost)} by default instead of www or another subdomain.

+ ${tabLine} +

You can switch between scopes later at the top of the results.

+
+ + ${tabButton} +
+
+ `); + bindWelcomeActions(); + bindSettingsFab(); +} + +function renderLoading(mode: CheckMode): void { + const targets = tabHostname ? resolveCheckTargets(tabHostname, mode) : null; + const rootTargets = tabHostname ? resolveCheckTargets(tabHostname, 'apex') : null; + const headerHost = targets?.queryHost ?? ''; + const showExact = rootTargets ? rootTargets.queryHost !== rootTargets.tab : true; root.innerHTML = shellWithFabFooterOnly(`
${headerHost ? renderHeaderBrand(headerHost) : '

JayQuery

'} - ${modeChips(mode)} + ${modeChips(mode, showExact)}

${escapeHtml(loadingLabel(mode, tabHostname))}

@@ -464,7 +499,8 @@ function dmarcHint(result: CheckResult): string { function renderResult(result: CheckResult): void { const { full } = result; const dkimRaw = result.dkim.raw; - const tabDiffers = result.tabHostname !== result.queryHostname; + const rootTargets = resolveCheckTargets(result.tabHostname, 'apex'); + const showExact = rootTargets.queryHost !== rootTargets.tab; const detailedBreakdown = settings.detailedBreakdown; const castShameModal = hasReportableDmarcIssue(result) ? renderCastShameModal(result) : ''; @@ -479,8 +515,7 @@ function renderResult(result: CheckResult): void {
${renderHeaderBrand(result.queryHostname)} - ${modeChips(result.mode)} - ${tabDiffers ? `

Root check uses the registrable domain; switch to Tab hostname to score ${escapeHtml(result.tabHostname)}.

` : ''} + ${modeChips(result.mode, showExact)}
@@ -682,6 +717,26 @@ function bindSettingsFab(): void { }); } +async function dismissWelcomeAndRun(mode: CheckMode): Promise { + settings = { ...settings, firstRunWelcomeSeen: true }; + try { + await saveSettings(settings); + } catch (err) { + console.error('settings: failed to save first-run welcome state', err); + } + currentView = 'main'; + await runCheck(mode); +} + +function bindWelcomeActions(): void { + document + .getElementById('btn-welcome-root') + ?.addEventListener('click', () => void dismissWelcomeAndRun('apex')); + document + .getElementById('btn-welcome-tab') + ?.addEventListener('click', () => void dismissWelcomeAndRun('exact')); +} + function partialNeedsDnsRefresh(partial: Partial): boolean { return ( partial.treatDnsResolutionErrorsAsFailure !== undefined || @@ -781,8 +836,13 @@ async function main(): Promise { } tabHostname = tab.host; activeTabId = tab.tabId; - currentView = 'main'; lastResult = null; + if (!settings.firstRunWelcomeSeen) { + currentView = 'welcome'; + renderWelcome(); + return; + } + currentView = 'main'; await runCheck('apex'); } diff --git a/entrypoints/popup/style.css b/entrypoints/popup/style.css index b89b19e..a0f200e 100644 --- a/entrypoints/popup/style.css +++ b/entrypoints/popup/style.css @@ -582,6 +582,93 @@ body { color: var(--muted); } +.welcome { + margin-top: 16px; + padding: 16px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); +} + +.welcome__kicker { + margin: 0 0 8px; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent2); +} + +.welcome__title { + margin: 0 0 10px; + font-size: 1.05rem; + line-height: 1.25; + font-weight: 650; + color: var(--text); +} + +.welcome__text { + margin: 0; + font-size: 0.82rem; + line-height: 1.45; + color: #b8becd; +} + +.welcome__text + .welcome__text { + margin-top: 8px; +} + +.welcome__text code { + font-family: ui-monospace, 'Cascadia Code', monospace; + font-size: 0.88em; + color: var(--text); +} + +.welcome__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.welcome__btn { + flex: 1; + min-width: 136px; + padding: 9px 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--surface2); + color: var(--text); + font-family: inherit; + font-size: 0.78rem; + font-weight: 700; + cursor: pointer; + transition: + background 0.15s, + border-color 0.15s, + filter 0.15s; +} + +.welcome__btn:hover { + border-color: rgba(91, 140, 255, 0.45); + background: var(--surface); +} + +.welcome__btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.welcome__btn--primary { + border-color: rgba(91, 140, 255, 0.45); + background: linear-gradient(165deg, #6b9cff 0%, var(--accent) 100%); + color: #fff; +} + +.welcome__btn--primary:hover { + filter: brightness(1.06); +} + .mono { font-family: ui-monospace, 'Cascadia Code', monospace; font-size: 0.88em; diff --git a/lib/checkDomain.test.ts b/lib/checkDomain.test.ts index 27a65c7..7785450 100644 --- a/lib/checkDomain.test.ts +++ b/lib/checkDomain.test.ts @@ -19,4 +19,10 @@ describe('resolveCheckTargets', () => { const r = resolveCheckTargets('www.EXAMPLE.co.uk', 'apex'); expect(r.queryHost).toBe('example.co.uk'); }); + + it('normalises trailing dots before comparing targets', () => { + const r = resolveCheckTargets('github.com.', 'apex'); + expect(r.tab).toBe('github.com'); + expect(r.queryHost).toBe('github.com'); + }); }); diff --git a/lib/checkDomain.ts b/lib/checkDomain.ts index 1aa96c3..f74961b 100644 --- a/lib/checkDomain.ts +++ b/lib/checkDomain.ts @@ -99,7 +99,7 @@ export function resolveCheckTargets( tabHostname: string, mode: CheckMode, ): { tab: string; orgDomain: string; queryHost: string } { - const tab = tabHostname.trim().toLowerCase(); + const tab = tabHostname.trim().toLowerCase().replace(/\.+$/, ''); const orgDomain = getDomain(tab, { detectIp: false }) ?? tab; const queryHost = mode === 'apex' ? orgDomain : tab; return { tab, orgDomain, queryHost }; diff --git a/lib/settings.ts b/lib/settings.ts index 9cd11c4..8c55451 100644 --- a/lib/settings.ts +++ b/lib/settings.ts @@ -30,6 +30,8 @@ export type ExtensionSettings = { * When false, only actionable lines (warn, fail, missing) are listed for compact results. */ detailedBreakdown: boolean; + /** Whether the root-vs-tab-host welcome screen has been dismissed. */ + firstRunWelcomeSeen: boolean; }; const STORAGE_KEY = 'jayquerySettings'; @@ -79,6 +81,7 @@ export const DEFAULT_SETTINGS: ExtensionSettings = { toolbarIconDriver: 'combined', dnsProvider: 'google', detailedBreakdown: false, + firstRunWelcomeSeen: false, }; export async function loadSettings(): Promise { @@ -97,6 +100,10 @@ export async function loadSettings(): Promise { typeof v?.detailedBreakdown === 'boolean' ? v.detailedBreakdown : DEFAULT_SETTINGS.detailedBreakdown, + firstRunWelcomeSeen: + typeof v?.firstRunWelcomeSeen === 'boolean' + ? v.firstRunWelcomeSeen + : DEFAULT_SETTINGS.firstRunWelcomeSeen, }; if (raw[STORAGE_KEY] === undefined && raw[LEGACY_STORAGE_KEY] !== undefined) { await browser.storage.local.set({ [STORAGE_KEY]: next });