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 `
@@ -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 {
@@ -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 });