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
+
+
+
+
+
-
+
+
+
+
-
+
Skip to content
@@ -58,21 +74,15 @@ A process bus lab.Without the test rack.
-
-
-
ARStack Studio / Process Bus ● LAB READY
-
-
PROCESS BUS TOPOLOGY REAL ETHERNET OUTPUT
-
ARStack StudioWaveform & SCL
ESP32-P4SV + PTP publisher
IED / AnalyzerLab subscriber
-
SV · 4I + 4V PTP · Grandmaster emulation SCL import
-
-
-
-
-
-
-
-
+
+
+
+
+
+ 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 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 Studio Extract the Windows Setup ZIP or run the Linux x86_64 AppImage.
Connect your board Use USB data and an isolated Ethernet test network.
Load matching firmware Install the release-paired pre-v3 or v3.x image from Studio.
Start SV + PTP Use 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"', '