Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 73 additions & 13 deletions entrypoints/popup/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
? `<button type="button" class="chip ${mode === 'exact' ? 'chip--active' : ''}" id="btn-mode-exact" ${mode === 'exact' ? 'aria-current="true"' : ''}>
Tab hostname
</button>`
: '';
return `
<div class="mode-row" role="group" aria-label="DNS query scope">
<button type="button" class="chip ${mode === 'apex' ? 'chip--active' : ''}" id="btn-mode-apex" ${mode === 'apex' ? 'aria-current="true"' : ''}>
Root domain
</button>
<button type="button" class="chip ${mode === 'exact' ? 'chip--active' : ''}" id="btn-mode-exact" ${mode === 'exact' ? 'aria-current="true"' : ''}>
Tab hostname
</button>
${exactChip}
</div>
`;
}
Expand All @@ -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
? `<p class="welcome__text">Use <strong>Tab hostname</strong> only when you want to test the exact subdomain shown in this tab: <span class="mono">${escapeHtml(tabHostname)}</span>.</p>`
: '';
const tabButton = tabDiffers
? '<button type="button" class="welcome__btn" id="btn-welcome-tab">Check tab hostname</button>'
: '';

root.innerHTML = shellWithFabFooterOnly(`
<header class="header">
${renderHeaderBrand(rootHost)}
</header>
<section class="welcome" aria-labelledby="welcome-title">
<p class="welcome__kicker">First run</p>
<h2 class="welcome__title" id="welcome-title">JayQuery starts at the root domain.</h2>
<p class="welcome__text">Most email security records are set on the main domain, so JayQuery checks <span class="mono">${escapeHtml(rootHost)}</span> by default instead of <code>www</code> or another subdomain.</p>
${tabLine}
<p class="welcome__text">You can switch between scopes later at the top of the results.</p>
<div class="welcome__actions">
<button type="button" class="welcome__btn welcome__btn--primary" id="btn-welcome-root">Check root domain</button>
${tabButton}
</div>
</section>
`);
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(`
<header class="header">
${headerHost ? renderHeaderBrand(headerHost) : '<h1 class="header__title header__title--solo">JayQuery</h1>'}
${modeChips(mode)}
${modeChips(mode, showExact)}
<p class="header__hint">${escapeHtml(loadingLabel(mode, tabHostname))}</p>
</header>
<div class="loading">
Expand Down Expand Up @@ -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) : '';
Expand All @@ -479,8 +515,7 @@ function renderResult(result: CheckResult): void {
<div class="shell shell--with-fab">
<header class="header">
${renderHeaderBrand(result.queryHostname)}
${modeChips(result.mode)}
${tabDiffers ? `<p class="header__hint">Root check uses the registrable domain; switch to <strong>Tab hostname</strong> to score <span class="mono">${escapeHtml(result.tabHostname)}</span>.</p>` : ''}
${modeChips(result.mode, showExact)}
</header>

<section class="hero">
Expand Down Expand Up @@ -682,6 +717,26 @@ function bindSettingsFab(): void {
});
}

async function dismissWelcomeAndRun(mode: CheckMode): Promise<void> {
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);
}
Comment thread
TJBK marked this conversation as resolved.

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<ExtensionSettings>): boolean {
return (
partial.treatDnsResolutionErrorsAsFailure !== undefined ||
Expand Down Expand Up @@ -781,8 +836,13 @@ async function main(): Promise<void> {
}
tabHostname = tab.host;
activeTabId = tab.tabId;
currentView = 'main';
lastResult = null;
if (!settings.firstRunWelcomeSeen) {
currentView = 'welcome';
renderWelcome();
return;
}
currentView = 'main';
await runCheck('apex');
}

Expand Down
87 changes: 87 additions & 0 deletions entrypoints/popup/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions lib/checkDomain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
2 changes: 1 addition & 1 deletion lib/checkDomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
7 changes: 7 additions & 0 deletions lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +81,7 @@ export const DEFAULT_SETTINGS: ExtensionSettings = {
toolbarIconDriver: 'combined',
dnsProvider: 'google',
detailedBreakdown: false,
firstRunWelcomeSeen: false,
};

export async function loadSettings(): Promise<ExtensionSettings> {
Expand All @@ -97,6 +100,10 @@ export async function loadSettings(): Promise<ExtensionSettings> {
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 });
Expand Down