Skip to content

Commit c19da89

Browse files
author
patj
committed
Wire download links directly to the latest release installers
os-detect.js -> downloads.js: after OS highlight, resolve the latest release via the GitHub API and point every download link at the exact installer asset (asset names carry the version, so we read them, not guess URLs). Validated against real v26.7.1 assets — incl. separate macOS Intel dmg. - Per-variant links carry data-dl asset patterns; hero button + card links get real browser_download_url; Windows stays on the MS Store - 'v26.7.1' chip becomes live from the real latest tag (data-latest-tag) - localStorage cache (1h) keeps well under GitHub's rate limit - Graceful fallback: API failure/offline/artifact-CSP keeps the existing /releases/latest links; no-JS still works - CSP: add connect-src https://api.github.com - build.py inlines downloads.js into the single-file artifact
1 parent a08cc11 commit c19da89

5 files changed

Lines changed: 131 additions & 43 deletions

File tree

_headers

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
X-Frame-Options: DENY
77
Referrer-Policy: strict-origin-when-cross-origin
88
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
9-
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
9+
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' https://api.github.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
1010

1111
# Fingerprinted/rarely-changing media can cache hard
1212
/assets/*

assets/js/downloads.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* AetherSDR download wiring — progressive enhancement.
2+
*
3+
* 1. Highlights the download card matching the visitor's OS (no network).
4+
* 2. Resolves the LATEST release from the GitHub API and points every
5+
* download link straight at the correct installer asset — the asset
6+
* filenames carry the version, so we read them rather than guess URLs.
7+
*
8+
* If the API is unavailable (offline, rate-limited, or the artifact CSP),
9+
* every link keeps its fallback href to the GitHub "latest release" page,
10+
* so nothing ever breaks.
11+
*/
12+
(function () {
13+
var REPO = 'aethersdr/AetherSDR';
14+
var API = 'https://api.github.com/repos/' + REPO + '/releases/latest';
15+
var STORE_URL = 'https://apps.microsoft.com/detail/9nc6bmwfn811';
16+
var CACHE_KEY = 'aetherLatestRelease';
17+
var TTL = 3600 * 1000; // 1 hour
18+
19+
function detectOS() {
20+
var uaData = navigator.userAgentData;
21+
var platform = (uaData && uaData.platform) || navigator.platform || '';
22+
var s = (platform + ' ' + (navigator.userAgent || '')).toLowerCase();
23+
if (/mac|iphone|ipad|ipod|darwin/.test(s)) return 'macos';
24+
if (/win/.test(s)) return 'windows';
25+
if (/linux|x11|android|cros/.test(s)) return 'linux';
26+
return null;
27+
}
28+
29+
var currentOS = detectOS();
30+
31+
function highlight() {
32+
if (!currentOS) return;
33+
var card = document.querySelector('.dl-card[data-os="' + currentOS + '"]');
34+
if (!card) return;
35+
card.classList.add('is-recommended');
36+
var badge = card.querySelector('.dl-reco');
37+
if (badge) badge.hidden = false;
38+
}
39+
40+
// First asset whose name contains one of the priority substrings.
41+
function pick(assets, patterns) {
42+
for (var p = 0; p < patterns.length; p++) {
43+
var pat = patterns[p].trim().toLowerCase();
44+
if (!pat) continue;
45+
for (var i = 0; i < assets.length; i++) {
46+
if (assets[i].name.toLowerCase().indexOf(pat) !== -1) return assets[i];
47+
}
48+
}
49+
return null;
50+
}
51+
52+
function wire(release) {
53+
if (!release || !release.assets) return;
54+
var assets = release.assets;
55+
56+
// Per-variant links carry their own asset patterns.
57+
var links = document.querySelectorAll('[data-dl]');
58+
for (var i = 0; i < links.length; i++) {
59+
var asset = pick(assets, links[i].getAttribute('data-dl').split(','));
60+
if (asset) links[i].href = asset.browser_download_url;
61+
}
62+
63+
// Hero primary button -> the detected OS's installer (Store on Windows).
64+
var primary = document.querySelector('[data-dl-primary]');
65+
if (primary && currentOS) {
66+
if (currentOS === 'windows') {
67+
primary.href = STORE_URL;
68+
primary.target = '_blank';
69+
primary.rel = 'noopener';
70+
} else {
71+
var patts = currentOS === 'macos'
72+
? ['apple-silicon.dmg', '.dmg']
73+
: ['x86_64.appimage', '.appimage'];
74+
var a = pick(assets, patts);
75+
if (a) primary.href = a.browser_download_url;
76+
}
77+
}
78+
79+
// Live "latest release" version label.
80+
if (release.tag_name) {
81+
var tag = release.tag_name.charAt(0) === 'v' ? release.tag_name : 'v' + release.tag_name;
82+
var tags = document.querySelectorAll('[data-latest-tag]');
83+
for (var j = 0; j < tags.length; j++) tags[j].textContent = tag;
84+
}
85+
}
86+
87+
function readCache() {
88+
try {
89+
var o = JSON.parse(localStorage.getItem(CACHE_KEY));
90+
return o && Date.now() - o.t < TTL ? o.r : null;
91+
} catch (e) { return null; }
92+
}
93+
function writeCache(r) {
94+
try { localStorage.setItem(CACHE_KEY, JSON.stringify({ t: Date.now(), r: r })); } catch (e) {}
95+
}
96+
97+
function loadRelease() {
98+
var cached = readCache();
99+
if (cached) { wire(cached); return; }
100+
if (!window.fetch) return;
101+
fetch(API, { headers: { Accept: 'application/vnd.github+json' } })
102+
.then(function (res) { if (!res.ok) throw new Error('github ' + res.status); return res.json(); })
103+
.then(function (release) {
104+
var slim = {
105+
tag_name: release.tag_name,
106+
assets: (release.assets || []).map(function (a) {
107+
return { name: a.name, browser_download_url: a.browser_download_url };
108+
})
109+
};
110+
writeCache(slim);
111+
wire(slim);
112+
})
113+
.catch(function () { /* keep fallback /releases/latest links */ });
114+
}
115+
116+
function init() { highlight(); loadRelease(); }
117+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
118+
else init();
119+
})();

assets/js/os-detect.js

Lines changed: 0 additions & 31 deletions
This file was deleted.

index.html

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ <h1>The native workstation for your <span class="grad-text">whole station</span>
6767
not just in front of the radio.
6868
</p>
6969
<div class="hero-cta">
70-
<a class="btn btn-primary" href="#download">
70+
<a class="btn btn-primary" href="#download" data-dl-primary>
7171
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>
7272
Download for your platform
7373
</a>
@@ -81,7 +81,7 @@ <h1>The native workstation for your <span class="grad-text">whole station</span>
8181
</a>
8282
</div>
8383
<div class="hero-meta">
84-
<span class="chip"><b>v26.7.1</b> · latest release</span>
84+
<span class="chip"><b data-latest-tag>v26.7.1</b> · latest release</span>
8585
<span class="chip">GPL v3</span>
8686
<span class="chip">Qt6 · C++20</span>
8787
<span class="chip">GPG-signed commits</span>
@@ -295,17 +295,17 @@ <h2>Native builds, released together</h2>
295295
<span class="dl-reco" hidden>Your platform</span>
296296
<div class="os"><svg viewBox="0 0 24 24"><path d="M12 2C9.5 2 8.7 4 8.7 6.2c0 1 .2 1.6.2 2.4-.5.6-2.4 2.9-2.4 6.1 0 2 .6 3.4 1.4 4.4.4.5.2 1.3-.4 1.6-.7.4-1.2 1-1.2 1.6 0 .8.9 1.2 2.2 1.2h6.6c1.3 0 2.2-.4 2.2-1.2 0-.6-.5-1.2-1.2-1.6-.6-.3-.8-1.1-.4-1.6.8-1 1.4-2.4 1.4-4.4 0-3.2-1.9-5.5-2.4-6.1 0-.8.2-1.4.2-2.4C15.3 4 14.5 2 12 2Z"/></svg> Linux</div>
297297
<div class="variants">
298-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">AppImage <span class="arch">x86_64</span></a>
299-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">AppImage <span class="arch">aarch64 · Pi</span></a>
298+
<a data-dl="x86_64.AppImage" href="https://github.com/aethersdr/AetherSDR/releases/latest">AppImage <span class="arch">x86_64</span></a>
299+
<a data-dl="aarch64.AppImage" href="https://github.com/aethersdr/AetherSDR/releases/latest">AppImage <span class="arch">aarch64 · Pi</span></a>
300300
</div>
301301
</div>
302302

303303
<div class="dl-card" data-os="macos">
304304
<span class="dl-reco" hidden>Your platform</span>
305305
<div class="os"><svg viewBox="0 0 24 24"><path d="M16.4 2c.1 1.2-.3 2.3-1 3.2-.8.9-2 1.6-3.1 1.5-.1-1.1.4-2.3 1.1-3.1.8-.9 2-1.5 3-1.6ZM19 17.3c-.5 1.2-.8 1.8-1.5 2.8-1 1.5-2.5 3.3-4.3 3.3-1.6 0-2-.9-4.1-.9-2.1 0-2.6.9-4.1.9-1.8 0-3.2-1.7-4.2-3.1C-1 16-1.4 9.8 1.4 6.9 2.7 5.4 4.5 4.5 6.3 4.5c1.8 0 3 1 4.1 1 1 0 2.4-1 4.3-1 1.4 0 3.4.6 4.7 2.4-4.1 2.3-3.5 8.2.6 10.4Z"/></svg> macOS</div>
306306
<div class="variants">
307-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">DMG <span class="arch">Apple Silicon</span></a>
308-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">DMG <span class="arch">Intel · Rosetta</span></a>
307+
<a data-dl="apple-silicon.dmg,.dmg" href="https://github.com/aethersdr/AetherSDR/releases/latest">DMG <span class="arch">Apple Silicon</span></a>
308+
<a data-dl="intel.dmg,x86_64.dmg,apple-silicon.dmg,.dmg" href="https://github.com/aethersdr/AetherSDR/releases/latest">DMG <span class="arch">Intel · Rosetta</span></a>
309309
</div>
310310
</div>
311311

@@ -314,8 +314,8 @@ <h2>Native builds, released together</h2>
314314
<div class="os"><svg viewBox="0 0 24 24"><path d="M3 5.5 10.5 4.4v7.1H3V5.5ZM11.6 4.2 21 3v8.5h-9.4V4.2ZM3 12.5h7.5v7.1L3 18.5v-6ZM11.6 12.5H21V21l-9.4-1.3v-7.2Z"/></svg> Windows</div>
315315
<div class="variants">
316316
<a class="primary-dl" href="https://apps.microsoft.com/detail/9nc6bmwfn811" target="_blank" rel="noopener">Microsoft Store <span class="arch">recommended</span></a>
317-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">Installer <span class="arch">x64 setup.exe</span></a>
318-
<a href="https://github.com/aethersdr/AetherSDR/releases/latest">Portable <span class="arch">x64 zip</span></a>
317+
<a data-dl="Windows-x64-setup.exe,setup.exe" href="https://github.com/aethersdr/AetherSDR/releases/latest">Installer <span class="arch">x64 setup.exe</span></a>
318+
<a data-dl="Windows-x64-portable.zip,portable.zip,portable" href="https://github.com/aethersdr/AetherSDR/releases/latest">Portable <span class="arch">x64 zip</span></a>
319319
</div>
320320
<div class="winget"><span class="prompt">&gt; </span>winget install aethersdr -s msstore</div>
321321
<p class="winget-note">Auto-updating via the Store, or one line for PowerShell users.</p>
@@ -489,6 +489,6 @@ <h4>Resources</h4>
489489
</div>
490490
</footer>
491491

492-
<script src="assets/js/os-detect.js?v=1" defer></script>
492+
<script src="assets/js/downloads.js?v=2" defer></script>
493493
</body>
494494
</html>

scripts/build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ def main():
3535

3636
# Inline JS as a real <script> before the asset pass — data-URI-ing a
3737
# script src would break execution in the single-file bundle.
38-
with open(os.path.join(ROOT, "assets/js/os-detect.js"), encoding="utf-8") as f:
38+
with open(os.path.join(ROOT, "assets/js/downloads.js"), encoding="utf-8") as f:
3939
js = f.read()
4040
html = re.sub(
41-
r'<script src="assets/js/os-detect\.js[^"]*"[^>]*></script>',
41+
r'<script src="assets/js/downloads\.js[^"]*"[^>]*></script>',
4242
f"<script>\n{js}\n</script>",
4343
html,
4444
)

0 commit comments

Comments
 (0)