diff --git a/.github/workflows/m3h-landing-browser-acceptance.yml b/.github/workflows/m3h-landing-browser-acceptance.yml index eb9b2cd49..230d35aca 100644 --- a/.github/workflows/m3h-landing-browser-acceptance.yml +++ b/.github/workflows/m3h-landing-browser-acceptance.yml @@ -3,9 +3,9 @@ name: ARStack Latest Stable Landing Browser and Mobile Acceptance on: push: branches: [main] - paths: ['index.html', '.github/workflows/m3h-landing-browser-acceptance.yml'] + paths: ['index.html', 'assets/screenshots/arstack-studio-windows.png', 'assets/studio_icon/**', 'scripts/check_site_visual_assets.py', '.github/workflows/m3h-landing-browser-acceptance.yml'] pull_request: - paths: ['index.html', '.github/workflows/m3h-landing-browser-acceptance.yml'] + paths: ['index.html', 'assets/screenshots/arstack-studio-windows.png', 'assets/studio_icon/**', 'scripts/check_site_visual_assets.py', '.github/workflows/m3h-landing-browser-acceptance.yml'] workflow_dispatch: permissions: @@ -17,6 +17,8 @@ jobs: timeout-minutes: 12 steps: - uses: actions/checkout@v4 + - name: Validate canonical visual asset sources and dimensions + run: python3 scripts/check_site_visual_assets.py - name: Install isolated browser automation shell: bash run: | @@ -40,20 +42,27 @@ jobs: const page=await browser.newPage({viewport:{width,height:900},deviceScaleFactor:1}); const response=await page.goto('http://127.0.0.1:4173/',{waitUntil:'domcontentloaded',timeout:30000}); if(response.status()!==200)throw new Error('LIVE_HTTP_'+response.status()); + await Promise.all([page.locator('.brand img').evaluate(el=>el.decode()),page.locator('.hero-visual img').evaluate(el=>el.decode())]); const data=await page.evaluate(()=>{ const buttons=[...document.querySelectorAll('a[data-asset="windows"],a[data-asset="portable"],a[data-asset="firmware"]')]; - const releaseLink=/^https:\/\/github\.com\/masarray\/arstack61850\/releases\/(latest|download\/[^/]+\/[^/]+)$/; + const releaseLink=/^https:\/\/github\.com\/masarray\/arstack61850\/releases\/(latest|tag\/[^/]+|download\/[^/]+\/[^/]+)$/; return {title:document.title, width:document.documentElement.clientWidth,scroll:document.documentElement.scrollWidth, latest:document.body.textContent.includes('M3.H.3') || document.body.textContent.includes('processbus-sim-m3h3-20261025'),benchmark:document.body.textContent.includes('4,705,090') && document.body.textContent.includes('M3.H.3') && document.body.textContent.includes('Three virtual Merging Units'), bodyPreview:document.body.textContent.slice(0,170), buttons:buttons.length, linksValid:buttons.every(el=>releaseLink.test(el.href)), anyVisible:buttons.some(el=>{const r=el.getBoundingClientRect();return r.width>0&&r.height>0}), linuxReady:!!document.querySelector('[data-asset="linux"]'), heroDownloads:[...document.querySelectorAll('.hero-downloads a')].map(el=>({label:el.textContent.trim(),href:el.href,visible:el.getBoundingClientRect().width>0&&el.getBoundingClientRect().height>0})), + officialBrand:(()=>{const el=document.querySelector('.brand img');return el?.naturalWidth===192&&el.getAttribute('src')==='assets/studio_icon/android-chrome-192x192.png'})(), + nativeScreenshot:(()=>{const el=document.querySelector('.hero-visual img');return el?.naturalWidth===960&&el?.naturalHeight===680&&el.getAttribute('src')==='assets/screenshots/arstack-studio-windows.png'})(), + studioFavicons:[...document.querySelectorAll('link[rel="icon"],link[rel="apple-touch-icon"],link[rel="manifest"]')].every(el=>el.getAttribute('href').startsWith('assets/studio_icon/')), + studioSocialImage:document.querySelector('meta[property="og:image"]')?.content?.endsWith('/assets/studio_icon/android-chrome-512x512.png')===true, + noPaintedMock:!document.querySelector('.monitor,.wave'), og:!!document.querySelector('meta[property="og:title"]')}; }); console.log(width,JSON.stringify(data)); await page.screenshot({path:'evidence/m3h-'+width+'.png',fullPage:true}); if(!data.latest||!data.benchmark||!data.og||data.buttons!==4||!data.anyVisible||!data.linksValid||!data.linuxReady)throw new Error('MISSING_LATEST_RELEASE_LINKS_'+width); if(data.heroDownloads.length!==2||!data.heroDownloads.every(x=>x.visible)||data.heroDownloads[0].label!=='Download for Windows'||data.heroDownloads[1].label!=='Download for Linux'||!/ARStack-Studio-win-x64-setup\.zip$/.test(data.heroDownloads[0].href)||!/ARStack-Studio-linux-x86_64\.AppImage$/.test(data.heroDownloads[1].href))throw new Error('HERO_DIRECT_PLATFORM_DOWNLOADS_'+width); + if(!data.officialBrand||!data.nativeScreenshot||!data.noPaintedMock||!data.studioFavicons||!data.studioSocialImage)throw new Error('NONCANONICAL_STUDIO_ICON_OR_MOCK_SCREEN_'+width); if(data.scroll>data.width+2)throw new Error('HORIZONTAL_OVERFLOW_'+width+':'+data.scroll+'>'+data.width); await page.close(); } diff --git a/.github/workflows/m3h-project-pages-publish.yml b/.github/workflows/m3h-project-pages-publish.yml index b7baec090..ca9a97bbd 100644 --- a/.github/workflows/m3h-project-pages-publish.yml +++ b/.github/workflows/m3h-project-pages-publish.yml @@ -5,6 +5,9 @@ on: branches: [main] paths: - 'index.html' + - 'assets/screenshots/arstack-studio-windows.png' + - 'assets/studio_icon/**' + - 'scripts/check_site_visual_assets.py' - '.github/workflows/m3h-project-pages-publish.yml' workflow_dispatch: @@ -47,10 +50,15 @@ jobs: grep -Fq 'ARStack-Studio-win-x64-setup.zip' index.html grep -Fq 'firmware-catalog.json' index.html grep -Fq 'PROCESSBUS_AUTONOMOUS_M3H3_OFFICIAL_20261025.md' index.html + python3 scripts/check_site_visual_assets.py mkdir -p _site cp index.html _site/index.html + mkdir -p _site/assets/screenshots _site/assets/studio_icon + for asset in android-chrome-192x192.png android-chrome-512x512.png favicon.ico favicon-16x16.png favicon-32x32.png apple-touch-icon.png site.webmanifest; do + test -s "assets/studio_icon/$asset" + cp "assets/studio_icon/$asset" "_site/assets/studio_icon/$asset" + done test -s assets/screenshots/arstack-studio-windows.png - mkdir -p _site/assets/screenshots cp assets/screenshots/arstack-studio-windows.png _site/assets/screenshots/arstack-studio-windows.png touch _site/.nojekyll - uses: actions/configure-pages@v5 @@ -74,9 +82,15 @@ jobs: grep -Fq 'data-asset="linux"' /tmp/arstack-live.html && grep -Fq 'Download for Windows' /tmp/arstack-live.html && grep -Fq 'Download for Linux' /tmp/arstack-live.html && + grep -Fq 'assets/studio_icon/android-chrome-192x192.png' /tmp/arstack-live.html && grep -Fq '4,705,090' /tmp/arstack-live.html && grep -Fq 'processbus-sim-dual-stable-20261105' /tmp/arstack-live.html; then - echo 'PASS: latest-release links deployed on the public product URL' + for asset in assets/studio_icon/android-chrome-192x192.png assets/studio_icon/android-chrome-512x512.png assets/studio_icon/favicon.ico assets/studio_icon/favicon-16x16.png assets/studio_icon/favicon-32x32.png assets/studio_icon/apple-touch-icon.png assets/studio_icon/site.webmanifest assets/screenshots/arstack-studio-windows.png; do + published="/tmp/arstack-live-$(basename "$asset")" + curl --fail --location --silent --show-error "https://masarray.github.io/arstack61850/$asset?build=$GITHUB_SHA" -o "$published" + cmp --silent "$asset" "$published" || { echo "FAIL: published image differs from repository $asset" >&2; exit 1; } + done + echo 'PASS: latest release links, original Studio icon family, manifest, and native Qt screenshot live on project Pages' exit 0 fi fi diff --git a/assets/studio_icon/site.webmanifest b/assets/studio_icon/site.webmanifest index 45dc8a206..38cb1362b 100644 --- a/assets/studio_icon/site.webmanifest +++ b/assets/studio_icon/site.webmanifest @@ -1 +1,20 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "ARStack61850 — ARStack Studio", + "short_name": "ARStack Studio", + "description": "IEC 61850 Process Bus Simulator", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#0a131e", + "background_color": "#0a131e", + "display": "standalone" +} diff --git a/index.html b/index.html index 2bdcde9c3..79879853a 100644 --- a/index.html +++ b/index.html @@ -8,37 +8,53 @@ ARStack61850 — IEC 61850 Process Bus Simulator + + + + + - + + + + - +
@@ -58,21 +74,15 @@

A process bus lab.
Without the test rack.

Windows x64 · Setup ZIP   /   Linux x86_64 · AppImage   ·   Release notes & checksums ↗
-
-
-
ARStack Studio / Process Bus● LAB READY
-
-
PROCESS BUS TOPOLOGYREAL ETHERNET OUTPUT
-
ARStack StudioWaveform & SCL
ESP32-P4SV + PTP publisher
IED / AnalyzerLab subscriber
-
SV · 4I + 4VPTP · Grandmaster emulationSCL import
- - - - - -
-
-
+
+ + Genuine native Windows Qt capture: ARStack SMV Injector showing 4I+4V setpoints, phasors, waveform traces, PTP source and capture controls + +
+ Actual Qt Studio capture, committed in this repository (not a mockup) + Capture provenance ↗ +
+
@@ -99,10 +109,6 @@

A process bus lab.
Without the test rack.

VERIFICATION BOUNDARY

Dual firmware, revision-aware provisioning

The stable Studio release offers separate firmware images for ESP32-P4 pre-v3 and v3.x silicon and a matching firmware catalog. v3.x board support remains an engineering preview pending broader physical acceptance.

See supported packages and release notes ↗
-
-
Desktop engineering workspace

Configure and observe from ARStack Studio.

Work with waveform controls, phasor views, stream configuration, firmware management and on-wire observations in a native desktop application.

-
Actual ARStack Studio Windows Qt desktop interface showing engineering controls, phasors and waveform views
Actual Windows Qt Studio screenshot captured by the native GUI test.
-
Fast onboarding

From download to first Ethernet packet.

Try the Studio interface without hardware, then attach a compatible ESP32-P4 to transmit real traffic.

First-run guide ↗
Install StudioExtract the Windows Setup ZIP or run the Linux x86_64 AppImage.
Connect your boardUse USB data and an isolated Ethernet test network.
Load matching firmwareInstall the release-paired pre-v3 or v3.x image from Studio.
Start SV + PTPUse 4I+4V Quick Start and a simulated PTP source, then capture the packets.
diff --git a/scripts/check_site_visual_assets.py b/scripts/check_site_visual_assets.py new file mode 100644 index 000000000..1b6c57d46 --- /dev/null +++ b/scripts/check_site_visual_assets.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Prevent invented landing branding: use ARStack's canonical Studio icon family. + +All public site identity must be from assets/studio_icon; the hero image is the +actual Qt UI screenshot from assets/screenshots. Stdlib only, fast in CI. +""" +from __future__ import annotations + +import json +import re +import struct +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HTML = (ROOT / "index.html").read_text(encoding="utf-8") +ICON_ROOT = "assets/studio_icon" +REQUIRED_PNG = { + f"{ICON_ROOT}/android-chrome-192x192.png": (192, 192), + f"{ICON_ROOT}/android-chrome-512x512.png": (512, 512), + f"{ICON_ROOT}/favicon-16x16.png": (16, 16), + f"{ICON_ROOT}/favicon-32x32.png": (32, 32), + "assets/screenshots/arstack-studio-windows.png": (960, 680), +} +OTHER = ( + f"{ICON_ROOT}/favicon.ico", + f"{ICON_ROOT}/apple-touch-icon.png", + f"{ICON_ROOT}/site.webmanifest", +) +errors: list[str] = [] + +for path, expected in REQUIRED_PNG.items(): + source = ROOT / path + if not source.is_file() or source.stat().st_size < 100: + errors.append(f"Missing or empty canonical product visual: {path}") + continue + header = source.read_bytes()[:24] + if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n" or header[12:16] != b"IHDR": + errors.append(f"Not a valid PNG header: {path}") + continue + size = struct.unpack(">II", header[16:24]) + if size != expected: + errors.append(f"Wrong dimensions for {path}: {size} vs {expected}") + +for path in OTHER: + if not (ROOT / path).is_file() or (ROOT / path).stat().st_size == 0: + errors.append(f"Missing canonical Studio identity asset: {path}") + +required_html = [ + rf']*>\s*', + r']*href="{ICON_ROOT}/favicon.ico"', + rf']*href="{ICON_ROOT}/favicon-16x16.png"', + rf']*href="{ICON_ROOT}/favicon-32x32.png"', + rf'', +] +for pattern in required_html: + if not re.search(pattern, HTML): + errors.append(f"Missing original Studio identity reference: {pattern}") + +for wrong in ('assets/favicon.ico', 'assets/favicon-96x96.png', + 'assets/web-app-manifest-', 'assets/site.webmanifest', + 'assets/apple-touch-icon.png', 'social-preview.svg', + 'class="monitor"', 'class="wave"', '