From d2c943e4929ec5358cedcfb2a73042fe1954b9f0 Mon Sep 17 00:00:00 2001 From: whoisaldo Date: Thu, 27 Aug 2026 14:27:19 -0400 Subject: [PATCH 1/4] Site: point Mac visitors at Sidecar instead of a dead end EternalMonitor is Windows-only, but nothing on the site said so above the fold, so Mac visitors had to read the subtitle or hit the download page to find out. Show them a dismissible notice naming the free native option that already solves their problem. Detection requires platform "Mac" AND maxTouchPoints <= 1: iPadOS reports "MacIntel" as its platform, and iPad users are the audience, not the ones to turn away. Dismissal is stored in sessionStorage, so it does not nag while browsing but returns on a fresh visit. --- docs/script.js | 19 +++++++++++++++++ docs/style.css | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/docs/script.js b/docs/script.js index 70c43c4..99d5def 100644 --- a/docs/script.js +++ b/docs/script.js @@ -3,6 +3,25 @@ (function () { 'use strict'; + /* --- macOS Notice --- */ + // iPadOS reports platform 'MacIntel' too, so require a non-touch device. + var isMac = /Mac/.test(navigator.platform) && navigator.maxTouchPoints <= 1; + if (isMac && sessionStorage.getItem('mac-notice-dismissed') !== '1') { + var notice = document.createElement('div'); + notice.className = 'mac-notice'; + notice.innerHTML = + '

Looks like you\'re on a Mac.' + + 'EternalMonitor is Windows-only for now. The good news: macOS already does this ' + + 'for free with Sidecar, ' + + 'which turns your iPad into a second display natively and works well.

' + + ''; + notice.querySelector('.mac-notice-close').addEventListener('click', function () { + sessionStorage.setItem('mac-notice-dismissed', '1'); + notice.remove(); + }); + document.body.appendChild(notice); + } + /* --- Scroll Reveal (IntersectionObserver) --- */ var reveals = document.querySelectorAll('.reveal'); if (reveals.length && 'IntersectionObserver' in window) { diff --git a/docs/style.css b/docs/style.css index 4436d96..842a71e 100644 --- a/docs/style.css +++ b/docs/style.css @@ -986,3 +986,58 @@ section { padding: 120px 0; } } + +/* --- macOS Notice Bar --- */ +.mac-notice { + position: fixed; + left: 16px; + right: 16px; + bottom: 16px; + z-index: 200; + display: flex; + align-items: flex-start; + gap: 16px; + max-width: 720px; + margin: 0 auto; + padding: 24px 28px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +.mac-notice p { + margin: 0; + font-size: 1rem; + line-height: 1.6; + color: var(--muted); +} + +.mac-notice strong { + display: block; + margin-bottom: 6px; + color: var(--text); + font-family: var(--font-display); + font-weight: 700; + font-size: 1.15rem; +} + +.mac-notice a { + color: var(--accent); +} + +.mac-notice-close { + flex-shrink: 0; + margin-left: auto; + padding: 4px 8px; + background: none; + border: none; + color: var(--muted); + font-size: 1.1rem; + line-height: 1; + cursor: pointer; +} + +.mac-notice-close:hover { + color: var(--text); +} From 73b9c3d33ec3da925682213f1e268718eb355445 Mon Sep 17 00:00:00 2001 From: whoisaldo Date: Thu, 27 Aug 2026 15:24:44 -0400 Subject: [PATCH 2/4] Support pre-release tags, and keep Metal debug info out of shipping builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testers need a real download link before the final release exists, but a v0.2.0-rc1 tag published as a normal release would become /releases/latest — which is exactly what the website reads, so an RC would be advertised to everyone. A semver pre-release tag (anything with a hyphen) now publishes with --prerelease: testers get a link, the site keeps pointing at the newest stable build. The release notes now also state the version-parity rule up front, since protocol v2 is a clean break and a mismatched pair is the most likely way a tester's first attempt fails. Separately, MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE sat in the target's base settings, so shader source and Metal debug info were compiled into release builds too, including anything uploaded to TestFlight. It is now debug-only. --- .github/workflows/release.yml | 22 +++++++++++++++++++--- ios/project.yml | 8 +++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a24530b..4961737 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,14 +113,30 @@ jobs: run: | $tag = $env:GITHUB_REF_NAME $sha = "${{ steps.hash.outputs.sha256 }}" + # A semver pre-release tag (v0.2.0-rc1, v0.2.0-beta2) publishes as a + # GitHub pre-release: testers get a real download link, while + # /releases/latest — which the website reads — keeps pointing at the + # newest stable build instead of advertising an RC to everyone. + $isPrerelease = $tag -match "-" $lines = @( "EternalMonitor $tag — see [RELEASE_NOTES.md](https://github.com/whoisaldo/EternalMonitor/blob/main/RELEASE_NOTES.md) for what's new.", "", + "**The Windows host and the iPad app must be from the same release.** Protocol v2 is a clean break: a v0.1.x app cannot stream from a v0.2.x host, or the reverse. Each side says so rather than showing broken video.", + "", "**EternalMonitor-Setup.exe SHA-256:**", '```', $sha, '```' ) - gh release create $tag "build\out\EternalMonitor-Setup.exe" ` - --title "EternalMonitor $tag" ` - --notes ($lines -join "`n") + # NOT $args — that is a PowerShell automatic variable. + $releaseArgs = @( + $tag, + "build\out\EternalMonitor-Setup.exe", + "--title", "EternalMonitor $tag", + "--notes", ($lines -join "`n") + ) + if ($isPrerelease) { + Write-Host "Tag $tag is a pre-release; it will not become /releases/latest." + $releaseArgs += "--prerelease" + } + gh release create @releaseArgs diff --git a/ios/project.yml b/ios/project.yml index f8387fe..a2c92c9 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -29,7 +29,13 @@ targets: CODE_SIGN_STYLE: Automatic ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor - MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE + configs: + # Shader source and Metal debug info belong in local builds only — + # under `base` this shipped inside every TestFlight/App Store build. + debug: + MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE + release: + MTL_ENABLE_DEBUG_INFO: NO EternalMonitorTests: type: bundle.unit-test From 986cca6d31d2a58dcd2052dae80b60a25a25231b Mon Sep 17 00:00:00 2001 From: whoisaldo Date: Thu, 27 Aug 2026 15:30:30 -0400 Subject: [PATCH 3/4] Website: offer the preview build to testers, clearly marked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The download page reads /releases/latest, which by definition skips pre-releases — so a build that exists only for testing was invisible there and testers had to be sent a link by hand. A preview card now appears when a pre-release exists, styled as a warning rather than an offer: it leads with "This is a test build, not a release", uses the quieter ghost button so it can't be mistaken for the stable download, and spells out that the Windows host and the iPad app must come from the same preview, since protocol v2 makes a mismatched pair fail. It carries the tag, file size and SHA-256 like the stable card, and hides itself again once a stable release supersedes it. --- docs/download.html | 32 ++++++++++++++++++++++++++++++++ docs/script.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ docs/style.css | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/docs/download.html b/docs/download.html index 3a26196..bc90af2 100644 --- a/docs/download.html +++ b/docs/download.html @@ -91,6 +91,38 @@

iPad App

+ + +
diff --git a/docs/script.js b/docs/script.js index 99d5def..460f49b 100644 --- a/docs/script.js +++ b/docs/script.js @@ -95,4 +95,49 @@ downloadBtn.textContent = 'View Releases on GitHub'; if (metaEl) metaEl.textContent = 'Visit GitHub for the latest release'; }); + + /* --- Preview build --- + /releases/latest above deliberately skips pre-releases, so testers would + never see a build that has not shipped yet. Look for the newest one and + reveal the preview card only if it exists; once a stable release + supersedes it, the card disappears on its own. */ + var previewSection = document.getElementById('preview-section'); + if (!previewSection) return; + + fetch('https://api.github.com/repos/whoisaldo/EternalMonitor/releases?per_page=10', { + headers: { 'Accept': 'application/vnd.github.v3+json' } + }) + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); + }) + .then(function (releases) { + var preview = releases.find(function (r) { + return r.prerelease && !r.draft; + }); + if (!preview) return; // nothing in testing right now + + var asset = preview.assets.find(function (a) { + return a.name.endsWith('.exe') || a.name.endsWith('.zip') || a.name.endsWith('.msi'); + }); + if (!asset) return; + + var btn = document.getElementById('preview-btn'); + var version = document.getElementById('preview-version'); + var meta = document.getElementById('preview-meta'); + var sha = document.getElementById('preview-sha256'); + + btn.href = asset.browser_download_url; + btn.textContent = 'Download ' + asset.name; + if (version) version.textContent = preview.tag_name; + if (meta) meta.textContent = asset.name + ' \u00B7 ' + formatBytes(asset.size); + if (sha && preview.body) { + var match = preview.body.match(/[a-fA-F0-9]{64}/); + if (match) sha.textContent = match[0]; + } + previewSection.hidden = false; + }) + .catch(function () { + /* No preview, or the API is unreachable: leave the card hidden. */ + }); })(); diff --git a/docs/style.css b/docs/style.css index 842a71e..d386116 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1041,3 +1041,41 @@ section { .mac-notice-close:hover { color: var(--text); } + +/* --- Preview build (pre-release, testers only) --- */ +.preview-section { + padding: 0 0 40px; +} + +.preview-card { + background: rgba(var(--accent-rgb), 0.03); + border: 1px solid rgba(var(--accent-rgb), 0.1); + border-left: 3px solid var(--accent); + border-radius: var(--radius-md); + padding: 28px; +} + +.preview-head { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.preview-head h2 { + font-size: 1.25rem; + margin: 0; +} + +.preview-warning { + color: var(--text); + font-size: 0.95rem; + margin-bottom: 10px; +} + +.preview-parity { + color: var(--muted); + font-size: 0.9rem; + margin-bottom: 22px; +} From 3d2b716d5f62ac9d3b6fdf09dad25ebc47af36f6 Mon Sep 17 00:00:00 2001 From: whoisaldo Date: Thu, 27 Aug 2026 15:31:30 -0400 Subject: [PATCH 4/4] Document TestFlight distribution and the version-parity trap FRIENDS_TESTING gains the full path for putting a build in a tester's hands without them needing Xcode: archive and upload (both the Xcode route and the CLI one), the App Store Connect steps, why the first external build waits on Beta App Review, and the two links a tester needs. QUICKSTART now names TestFlight explicitly and leads with the rule that bites hardest, which is that the host and the app must be the same version. --- FRIENDS_TESTING.md | 70 ++++++++++++++++++++++++++++++++++++++++++ scripts/QUICKSTART.txt | 7 ++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/FRIENDS_TESTING.md b/FRIENDS_TESTING.md index 170cd08..68ef9fc 100644 --- a/FRIENDS_TESTING.md +++ b/FRIENDS_TESTING.md @@ -13,6 +13,76 @@ steps. To bundle the driver, drop the signed setup into `installer/vendor/vdd/` that folder's `README.txt`); without it the build still works but produces an app-only installer and the extended-display option won't appear. +## Getting a tester onto the iPad app (TestFlight) + +A tester needs no Xcode and no developer account. They need the TestFlight +app from the App Store and a link from you. + +### One-time, on your Mac + +Build number must be higher than anything already uploaded. `ios/project.yml` +carries `CURRENT_PROJECT_VERSION`; builds 3 and 4 are used, so 0.2.0 ships as +build 5. If you upload twice for the same version, bump it again. + +The simplest path is Xcode: + +1. `cd ios && xcodegen generate`, then open `EternalMonitor.xcodeproj`. +2. Select "Any iOS Device" as the destination, then Product, then Archive. +3. In the Organizer window that opens: Distribute App, then TestFlight and + App Store Connect, then Upload. Signing is automatic against team + `9X79V37Q89`. + +The same thing without the GUI, if you have an App Store Connect API key: + +```bash +cd ios +xcodegen generate +xcodebuild -project EternalMonitor.xcodeproj -scheme EternalMonitor \ + -configuration Release -destination 'generic/platform=iOS' \ + -archivePath build/EternalMonitor.xcarchive archive +xcodebuild -exportArchive \ + -archivePath build/EternalMonitor.xcarchive \ + -exportOptionsPlist exportOptions.plist \ + -exportPath build/export \ + -authenticationKeyPath ~/private_keys/AuthKey_XXXXXX.p8 \ + -authenticationKeyID XXXXXX -authenticationKeyIssuerID +``` + +`exportOptions.plist` is already set to `app-store-connect` with +`destination: upload`, so the export step performs the upload. + +### Then, in App Store Connect + +1. TestFlight tab, wait for the build to finish processing (usually a few + minutes; you get an email). +2. Export compliance is already answered in the app + (`ITSAppUsesNonExemptEncryption` is false), so it will not ask per build. +3. Create an external testing group, add the build, and fill in "What to + Test" (see the AMD notes below). +4. Submit for Beta App Review. The first build for external testers is + reviewed by Apple, usually inside a day. +5. Once approved, enable the group's public link and send that to your + tester. Anyone with the link can install; you can cap the number of + testers on the same screen. + +Internal testers skip review entirely and get builds immediately, but they +must be users on your App Store Connect team, so that route only makes sense +for people you want inside the developer account. + +### What to send the tester + +Two links, and they must match: + +- The Windows installer for the same version. While a build is still in + testing it is published as a GitHub pre-release and appears on + eternalmonitor.dev/download.html under "Preview build for testers", + marked as a test build. +- The TestFlight public link for the matching iPad build. + +Protocol v2 is a clean break, so a preview host with a release app (or the +reverse) will not stream. Both sides say so plainly rather than showing +broken video, but it still wastes a tester's evening. + ## Extended display vs mirror By default the iPad mirrors the primary screen. To test the iPad as a real extended desktop: diff --git a/scripts/QUICKSTART.txt b/scripts/QUICKSTART.txt index 7fba62a..75e66cf 100644 --- a/scripts/QUICKSTART.txt +++ b/scripts/QUICKSTART.txt @@ -3,7 +3,12 @@ EternalMonitor - Quick Start (Windows host) This turns your Windows PC into a second screen for your iPad over your local Wi-Fi. This is the WINDOWS side only. You also need the EternalMonitor -app on an iPad (the sender will give you a separate invite for that). +app on the iPad, which arrives as a TestFlight invite link: install Apple's +free "TestFlight" app from the App Store first, then open the link. + +IMPORTANT: the Windows host and the iPad app must be from the SAME version. +If they are not, the app will tell you to update one of them instead of +showing video. What you need -------------