From 4d02a892b6c800ab24a43179f783abf1e1d32b8e Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Fri, 16 Oct 2026 10:55:11 -0400 Subject: [PATCH 1/2] feat(site): support embedding scans in the main website Add an origin-checked parent handshake for headerless dark-mode rendering, responsive iframe sizing, and top-level navigation. Preserve standalone behavior and cover messaging with CI tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- README.md | 6 +++- site/embed.js | 40 ++++++++++++++++++++++ site/index.html | 1 + site/styles.css | 4 ++- tests/embed.test.cjs | 74 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 site/embed.js create mode 100644 tests/embed.test.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f2e2d7..367828b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: if-no-files-found: warn - name: Test landing page rendering shell: powershell - run: node --test .\tests\site.test.cjs + run: node --test .\tests\site.test.cjs .\tests\embed.test.cjs required-checks: name: Required checks diff --git a/README.md b/README.md index 4b8470a..ac07454 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,10 @@ Website source is in [site/](site/); the only loader source is [scans.ps1](scans The landing page follows the PrintMaster site's Solarized palette, teal accents, wordmark, panel styling, and cross-site navigation. Styles and the logo are local to this repository; navigation works without JavaScript, and the layout supports light/dark preferences, keyboard navigation, and reduced motion. Release rendering and copy buttons use [site/releases.js](site/releases.js). +The main website can frame `https://scans.printmaster.work/?embed=1` at `/scans/`. [site/embed.js](site/embed.js) hides this site's header/footer and locks the dark palette only after a handshake from the immediate parent at `https://printmaster.work`. Without that handshake (including direct visits and JavaScript disabled), the full standalone site remains visible. Origin- and source-checked messages report content height and anchor positions to the main site's wrapper. External links open at the top level; setup commands retain their canonical standalone URLs. The main site owns its header/footer and clipboard delegation. Deploy this embed support before the main-site wrapper. + +The handshake restricts embed-mode activation, not who can frame the public standalone site. GitHub Pages cannot set a custom HTTP `Content-Security-Policy: frame-ancestors` header; a meta tag cannot enforce it. If the domain later uses a configurable edge/server, set `frame-ancestors 'self' https://printmaster.work` there and avoid `X-Frame-Options: DENY`/`SAMEORIGIN`, which would block the cross-origin main-site frame. + Work on a feature branch and open a pull request. The [CI workflow](.github/workflows/ci.yml) runs syntax checks and mocked Pester tests on Windows using both Windows PowerShell 5.1 and PowerShell 7. Its **Required checks** job passes only when both test jobs succeed, including when using a merge queue. CI also runs after pushes to `main` and can be started manually. External actions are pinned to full commit SHAs. [Dependabot](.github/dependabot.yml) checks GitHub Actions dependencies weekly and opens update PRs; updates must pass the same required CI. @@ -135,7 +139,7 @@ From the repository root, run either runtime: ```powershell powershell.exe -NoProfile -STA -File .\tests\Invoke-Tests.ps1 pwsh.exe -NoProfile -STA -File .\tests\Invoke-Tests.ps1 -node --test .\tests\site.test.cjs +node --test .\tests\site.test.cjs .\tests\embed.test.cjs ``` The [test runner](tests/Invoke-Tests.ps1) fails on syntax errors, failing tests, or an empty test run. The [regression suite](tests/Scans.Tests.ps1) extracts function definitions without running the utility's elevation or setup code. Account, registry, filesystem, SMB, firewall, clipboard-related setup gating, and COM changes are mocked; WPF windows are loaded but never displayed. Tests cover password failures, error-popup routing, account visibility, folder setup, shares, network configuration, shortcuts, generated password shape, and setup options. The generator tests do not claim that every generated password satisfies policy. diff --git a/site/embed.js b/site/embed.js new file mode 100644 index 0000000..ffb303c --- /dev/null +++ b/site/embed.js @@ -0,0 +1,40 @@ +(() => { + const origin = 'https://printmaster.work'; + if (window.parent === window || new URLSearchParams(location.search).get('embed') !== '1') return; + let embedded = false; + const send = data => window.parent.postMessage(data, origin); + const resize = () => send({ + type: 'printmaster:scans:resize', + height: Math.ceil(document.body.getBoundingClientRect().height) + }); + const navigate = hash => { + if (typeof hash !== 'string' || !/^#[a-z][a-z0-9-]*$/.test(hash)) return; + const target = document.getElementById(hash.slice(1)); + if (target) send({ + type: 'printmaster:scans:anchor', hash, + top: target.getBoundingClientRect().top + window.scrollY + }); + }; + window.addEventListener('message', event => { + if (event.origin !== origin || event.source !== window.parent || + !event.data || event.data.type !== 'printmaster:scans:init') return; + if (!embedded) { + embedded = true; + document.documentElement.setAttribute('data-embedded', ''); + document.querySelectorAll('a[href^="https://"]').forEach(link => { + link.target = '_top'; + }); + new ResizeObserver(resize).observe(document.body); + document.addEventListener('click', event => { + const link = event.target.closest('a[href^="#"]'); + if (!link || event.defaultPrevented || event.button !== 0 || + event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; + event.preventDefault(); + navigate(link.getAttribute('href')); + }); + } + resize(); + navigate(event.data.hash); + }); + send({ type: 'printmaster:scans:ready' }); +})(); diff --git a/site/index.html b/site/index.html index 149dfff..5cfde12 100644 --- a/site/index.html +++ b/site/index.html @@ -9,6 +9,7 @@ + diff --git a/site/styles.css b/site/styles.css index 8632c87..1344a84 100644 --- a/site/styles.css +++ b/site/styles.css @@ -100,8 +100,10 @@ pre { padding: 1rem; background: var(--code); border: 1px solid var(--border); b .footer-top nav a:hover { color: var(--accent); } .footer-bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; border-top: 1px solid var(--border); padding-block: 1.25rem; font-size: .75rem; } @media (prefers-color-scheme: light) { - :root { --bg: #fdf6e3; --bg-deep: #f5eedb; --panel: #eee8d5; --border: #d3cbb8; --fg: #073642; --muted: #586e75; --accent: #006b61; --button: #007b70; --button-fg: #fff; --code: #fdf6e3; } + :root:not([data-embedded]) { --bg: #fdf6e3; --bg-deep: #f5eedb; --panel: #eee8d5; --border: #d3cbb8; --fg: #073642; --muted: #586e75; --accent: #006b61; --button: #007b70; --button-fg: #fff; --code: #fdf6e3; } } +:root[data-embedded] { color-scheme: dark; scroll-padding-top: 0; } +[data-embedded] .site-header, [data-embedded] .site-footer, [data-embedded] .skip-link { display: none; } @media (max-width: 960px) { .hero-layout { gap: 2rem; } .setup-layout { grid-template-columns: 1fr; gap: 2rem; } diff --git a/tests/embed.test.cjs b/tests/embed.test.cjs new file mode 100644 index 0000000..6dc737e --- /dev/null +++ b/tests/embed.test.cjs @@ -0,0 +1,74 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const source = fs.readFileSync(path.join(__dirname, '..', 'site', 'embed.js'), 'utf8'); + +function render(search = '?embed=1', framed = true) { + const messages = []; + const listeners = {}; + const attributes = {}; + const external = {}; + let observer; + const parent = { postMessage: (data, origin) => messages.push({ data, origin }) }; + const window = { parent, scrollY: 0, addEventListener: (type, fn) => { listeners[type] = fn; } }; + if (!framed) window.parent = window; + vm.runInNewContext(source, { + window, location: { search }, URLSearchParams, + ResizeObserver: class { + constructor(fn) { observer = fn; } + observe() {} + }, + document: { + body: { getBoundingClientRect: () => ({ height: 1800 }) }, + documentElement: { setAttribute: (key, value) => { attributes[key] = value; } }, + querySelectorAll: () => [external], + getElementById: id => id === 'releases' ? { getBoundingClientRect: () => ({ top: 1200 }) } : null, + addEventListener: (type, fn) => { listeners[type] = fn; } + } + }); + return { messages, listeners, attributes, external, parent, resize: () => observer() }; +} + +test('standalone and unrequested frames never hide navigation or initialize messaging', () => { + for (const page of [render('', true), render('?embed=1', false)]) { + assert.equal(page.messages.length, 0); + assert.equal(page.listeners.message, undefined); + assert.deepEqual(page.attributes, {}); + } +}); + +test('embed mode requires the exact trusted parent and reports size and valid anchors', () => { + const page = render(); + assert.equal(page.messages[0].origin, 'https://printmaster.work'); + assert.deepEqual(page.attributes, {}); + for (const event of [ + { origin: 'https://evil.example', source: page.parent }, + { origin: 'https://printmaster.work', source: {} } + ]) page.listeners.message({ ...event, data: { type: 'printmaster:scans:init' } }); + assert.deepEqual(page.attributes, {}); + const init = hash => page.listeners.message({ + origin: 'https://printmaster.work', source: page.parent, + data: { type: 'printmaster:scans:init', hash } + }); + init('#releases'); + assert.equal(page.attributes['data-embedded'], ''); + assert.equal(page.external.target, '_top'); + assert.equal(page.messages[1].data.height, 1800); + assert.equal(page.messages[2].data.top, 1200); + assert.equal(page.messages[2].data.hash, '#releases'); + page.resize(); + assert.equal(page.messages.at(-1).data.type, 'printmaster:scans:resize'); + const count = page.messages.length; + init('#missing'); + init('javascript:alert(1)'); + assert.equal(page.messages.length, count + 2, 'invalid targets emit size only, never scroll'); + let prevented = false; + page.listeners.click({ + target: { closest: () => ({ getAttribute: () => '#releases' }) }, + button: 0, preventDefault: () => { prevented = true; } + }); + assert.equal(prevented, true); + assert.equal(page.messages.at(-1).data.type, 'printmaster:scans:anchor'); +}); From 4098c988b357f771b8d7554a0fe81fc1f80a2e43 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Fri, 16 Oct 2026 10:57:03 -0400 Subject: [PATCH 2/2] ux(site): clarify standalone scans navigation and external links Keep setup and release navigation local to Scans and separate explicitly labeled fleet-monitoring and Agent/Server download links in the footer. Preserve embedded navigation behavior and add regression assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- site/index.html | 19 +++++++++---------- tests/site.test.cjs | 13 ++++++++++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ac07454..e0320f0 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Pushing a stable SemVer tag starts the [Prepare release workflow](.github/workfl Website source is in [site/](site/); the only loader source is [scans.ps1](scans.ps1). The site builder copies it unchanged into its deployment output. The actual utility is [src/scans.ps1](src/scans.ps1); release packaging selects that file, never the loader. New stable tags must use `vMAJOR.MINOR.PATCH`; historical four-part tags are retained and linked through GitHub Releases, but not exposed through the new checksum-verified channel. -The landing page follows the PrintMaster site's Solarized palette, teal accents, wordmark, panel styling, and cross-site navigation. Styles and the logo are local to this repository; navigation works without JavaScript, and the layout supports light/dark preferences, keyboard navigation, and reduced motion. Release rendering and copy buttons use [site/releases.js](site/releases.js). +The landing page follows the PrintMaster site's Solarized palette, teal accents, wordmark, and panel styling. Standalone navigation is Scans-focused: its brand links home locally, setup and release links stay on the page, and external Scans resources are explicitly labeled. The footer separates Agent/Server downloads and fleet-monitoring links under "More from PrintMaster." Styles and the logo are local to this repository; navigation works without JavaScript, and the layout supports light/dark preferences, keyboard navigation, and reduced motion. Release rendering and copy buttons use [site/releases.js](site/releases.js). The main website can frame `https://scans.printmaster.work/?embed=1` at `/scans/`. [site/embed.js](site/embed.js) hides this site's header/footer and locks the dark palette only after a handshake from the immediate parent at `https://printmaster.work`. Without that handshake (including direct visits and JavaScript disabled), the full standalone site remains visible. Origin- and source-checked messages report content height and anchor positions to the main site's wrapper. External links open at the top level; setup commands retain their canonical standalone URLs. The main site owns its header/footer and clipboard delegation. Deploy this embed support before the main-site wrapper. diff --git a/site/index.html b/site/index.html index 5cfde12..b332a66 100644 --- a/site/index.html +++ b/site/index.html @@ -15,13 +15,12 @@ @@ -106,9 +105,9 @@

If something goes wrong