diff --git a/site/site-v6-core.js b/site/site-v6-core.js index 5d9511e..c8fd088 100644 --- a/site/site-v6-core.js +++ b/site/site-v6-core.js @@ -105,6 +105,18 @@ setLink(document.getElementById('mac-dmg-link'), state.macDmgUrl, Boolean(state.macDmgUrl)); setLink(document.getElementById('mac-vst3-link'), state.macVst3Url, Boolean(state.macVst3Url)); setLink(document.getElementById('mac-standalone-link'), state.macStandaloneUrl, Boolean(state.macStandaloneUrl)); + + const heroMac = document.getElementById('mac-dmg-link-hero'); + if (heroMac && state.macDmgUrl) { + setLink(heroMac, state.macDmgUrl, true); + } else if (heroMac) { + // Never regress to a missing Mac action: retain a useful static fallback + // even when release metadata is temporarily unavailable. + heroMac.href = '#download'; + heroMac.removeAttribute('aria-disabled'); + heroMac.removeAttribute('data-release-pending'); + } + setLink(document.getElementById('checksums-link'), state.checksumsUrl, Boolean(state.checksumsUrl)); setLink(document.getElementById('release-link'), state.releaseUrl || RELEASE_FALLBACK, true); setLink(document.getElementById('distribution-link'), state.releaseUrl || RELEASE_FALLBACK, true); @@ -181,4 +193,4 @@ setupMobileNavigation(); setupMobileDownload(); resolveRelease().then(renderRelease).catch(() => renderRelease(null)); -})(); +})(); \ No newline at end of file diff --git a/site/site-v6.js b/site/site-v6.js index ad55bdf..8b17ddc 100644 --- a/site/site-v6.js +++ b/site/site-v6.js @@ -23,6 +23,26 @@ }); } + // Keep both primary platform CTAs visible immediately. The Mac CTA uses a + // safe #download fallback first; the idle release runtime upgrades it to the + // exact reviewed DMG URL from release.json when that manifest is available. + const ensureHeroMacCta = () => { + if (document.getElementById('mac-dmg-link-hero')) return; + const actions = document.querySelector('.landing-hero .hero-copy .actions'); + if (!actions) return; + + const link = document.createElement('a'); + link.id = 'mac-dmg-link-hero'; + link.className = 'button secondary hero-mac-download'; + link.href = '#download'; + link.innerHTML = `${isIndonesian ? 'Unduh gratis untuk Mac' : 'Download free for Mac'}`; + + const explore = actions.querySelector('a[href^="#"]'); + actions.insertBefore(link, explore || null); + }; + + ensureHeroMacCta(); + const source = currentScript?.src || new URL(`${root.dataset.siteBase || '.'}/site-v6.js`, location.href).href; @@ -44,4 +64,4 @@ } else { window.requestAnimationFrame(() => window.setTimeout(loadCore, 0)); } -})(); +})(); \ No newline at end of file diff --git a/tools/validate-p0-release-boundary.py b/tools/validate-p0-release-boundary.py index 818e4c0..62fff0d 100644 --- a/tools/validate-p0-release-boundary.py +++ b/tools/validate-p0-release-boundary.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate the public release and GitHub Pages security boundary.""" +"""Validate the public release, Pages security boundary and platform CTA contract.""" from __future__ import annotations @@ -11,6 +11,12 @@ RELEASE_WORKFLOW = WORKFLOWS / "build-macos-and-publish.yml" PAGES_WORKFLOW = WORKFLOWS / "pages.yml" UPDATER = ROOT / "tools" / "update-public-crossplatform-release.py" +LANDING_EN = ROOT / "site" / "index.html" +LANDING_ID = ROOT / "site" / "id" / "index.html" +SITE_LOADER = ROOT / "site" / "site-v6.js" +SITE_CORE = ROOT / "site" / "site-v6-core.js" +HARDENING_CSS = ROOT / "site" / "hardening-v6.css" +RELEASE_MANIFEST = ROOT / "site" / "release.json" APPROVED_WORKFLOWS = [ ".github/workflows/build-macos-and-publish.yml", @@ -222,6 +228,40 @@ def validate_pages_workflow(text: str) -> None: ) +def validate_platform_cta_contract() -> None: + for path in (LANDING_EN, LANDING_ID, SITE_LOADER, SITE_CORE, HARDENING_CSS, RELEASE_MANIFEST): + require(path.is_file(), f"Platform CTA contract file is missing: {path.relative_to(ROOT)}") + + landing_en = LANDING_EN.read_text(encoding="utf-8") + landing_id = LANDING_ID.read_text(encoding="utf-8") + loader = SITE_LOADER.read_text(encoding="utf-8") + core = SITE_CORE.read_text(encoding="utf-8") + css = HARDENING_CSS.read_text(encoding="utf-8") + manifest = RELEASE_MANIFEST.read_text(encoding="utf-8") + + require( + 'id="installer-link-bottom"' in landing_en and "Download free for Windows" in landing_en, + "English Windows hero CTA is missing.", + ) + require( + 'id="installer-link-bottom"' in landing_id and "Unduh gratis untuk Windows" in landing_id, + "Indonesian Windows hero CTA is missing.", + ) + require("ensureHeroMacCta" in loader, "Mac hero CTA bootstrap is missing from the lightweight loader.") + require("mac-dmg-link-hero" in loader, "Mac hero CTA stable id is missing from the lightweight loader.") + require("Download free for Mac" in loader, "English Mac hero CTA label is missing.") + require("Unduh gratis untuk Mac" in loader, "Indonesian Mac hero CTA label is missing.") + require("link.hidden" not in loader, "Mac hero CTA must not be hidden by the bootstrap loader.") + require( + "document.getElementById('mac-dmg-link-hero')" in core + and "setLink(heroMac, state.macDmgUrl, true)" in core, + "Release runtime does not upgrade the Mac hero CTA to the reviewed DMG URL.", + ) + require("heroMac.href = '#download'" in core, "Mac hero CTA has no resilient download-section fallback.") + require(".button.secondary.hero-mac-download" in css, "Mac hero CTA visual treatment is missing.") + require('"macos-universal"' in manifest and '"macDmgUrl"' in manifest, "macOS release manifest contract is missing.") + + def main() -> int: require(RELEASE_WORKFLOW.is_file(), "Approved public release workflow is missing.") require(PAGES_WORKFLOW.is_file(), "Approved Pages workflow is missing.") @@ -230,13 +270,14 @@ def main() -> int: validate_workflow_inventory() validate_release_workflow(RELEASE_WORKFLOW.read_text(encoding="utf-8")) validate_pages_workflow(PAGES_WORKFLOW.read_text(encoding="utf-8")) + validate_platform_cta_contract() print( - "[PASS] Public build is read-only; publish is source-free; " - "Pages is exact-source validated; workflow inventory is allowlisted." + "[PASS] Public build is read-only; publish is source-free; Pages is exact-source validated; " + "workflow inventory is allowlisted; Windows and Mac hero CTAs are regression-guarded." ) return 0 if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file