diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9a54a6d1..1ee2943e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -37,6 +37,13 @@ jobs: test -f docs/screenshot/analyzer-overview.webp grep -q 'Process Bus Insight' docs/index.html + - name: Validate latest stable EXE download contract + shell: bash + run: | + set -euo pipefail + python3 scripts/verify-landing-release.py + node --check docs/app.js + - name: Configure Pages uses: actions/configure-pages@v6 diff --git a/README.md b/README.md index f6dc2f75..67a4a0b6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CodeQL](https://github.com/masarray/DigSubAnalyzer/actions/workflows/codeql.yml/badge.svg)](https://github.com/masarray/DigSubAnalyzer/actions/workflows/codeql.yml) [![GitHub Pages](https://github.com/masarray/DigSubAnalyzer/actions/workflows/pages.yml/badge.svg)](https://github.com/masarray/DigSubAnalyzer/actions/workflows/pages.yml) [![Release Package](https://github.com/masarray/DigSubAnalyzer/actions/workflows/release-package.yml/badge.svg)](https://github.com/masarray/DigSubAnalyzer/actions/workflows/release-package.yml) -[![Latest Release](https://img.shields.io/github/v/release/masarray/DigSubAnalyzer?include_prereleases&label=release)](https://github.com/masarray/DigSubAnalyzer/releases) +[![Latest Stable Release](https://img.shields.io/github/v/release/masarray/DigSubAnalyzer?label=stable)](https://github.com/masarray/DigSubAnalyzer/releases/latest) [![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Windows%2010%2F11-0078d4)](#download-and-run) [![.NET](https://img.shields.io/badge/.NET-8.0-512BD4)](#build-from-source) @@ -28,7 +28,7 @@ Process Bus Insight is intentionally receive-only. It does not send IEC 61850 co | SV analyzer | Multi-stream discovery, APPID/svID/MAC/VLAN/confRev evidence, selected-stream workspace, decoded waveform, RMS, phasor, sequence diagnostics, shape/distortion indication, and selectable scope windows. | | Runtime snapshots | Immutable selected-stream generations containing copied identity, waveform, analog, phasor, shape, and diagnostic evidence for coherent consumer reads. | | PCAP replay | Bounded classic Ethernet PCAP replay through the same decoder/analyzer entry point used by live Npcap capture. | -| GOOSE inspector | Publisher discovery, stNum/sqNum tracking, event timeline, typed `allData` decoding, change summaries, and SCL-assisted semantic context. | +| GOOSE inspector | Stable multi-publisher selection, physical-key Messages filtering, bounded decoded-change timeline, Follow live switch, red unread notification dots, unified message acknowledgement, typed DataSet value-change tooltips, and SCL-assisted signal/quality context. | | PTP visibility | Passive PTPv2 context, grandmaster/domain evidence where available, freshness wording, and timestamp-confidence boundaries. | | SCL validation | Load SCD/ICD/CID files and compare expected publishers/streams against observed APPID, destination MAC, VLAN, svID, confRev, and related evidence. | | Evidence workflow | Copyable engineering evidence, cautious timing language, target-aware diagnostics, and screenshot-friendly workspaces. | @@ -46,14 +46,16 @@ The application can record what was configured in SCL, what the selected capture ## Download and run -1. Download the latest published **Windows Inno Setup installer** (v1.4.2 after its public release) from [GitHub Releases](https://github.com/masarray/DigSubAnalyzer/releases): `ProcessBusInsight-v1.4.2-win-x64-setup.exe`. +**[Download the latest stable installer (.exe) — v1.4.2](https://github.com/masarray/DigSubAnalyzer/releases/download/v1.4.2/ProcessBusInsight-v1.4.2-win-x64-setup.exe)** · [Landing page](https://masarray.github.io/DigSubAnalyzer/) · [Latest GitHub Release](https://github.com/masarray/DigSubAnalyzer/releases/latest) + +1. Download the public stable **Windows Inno Setup installer** directly from the link above. The landing page automatically resolves the newest stable Windows setup EXE and keeps v1.4.2 as a working fallback. 2. Run the installer. Its stable app identity supports in-place updates, same-version repair, Windows uninstall, and optional desktop shortcut. 3. **Offline manuals are installed directly inside the app folder** (`User Manual.pdf`, `Quick Start.pdf`, and `Install and Update.md`), with Start Menu shortcuts. 4. Install **Npcap separately** when live raw Ethernet capture is required; it is not bundled. Offline PCAP analysis remains available without it. 5. Launch Process Bus Insight, select an authorized TAP or mirror capture adapter, and start analysis. 6. To update, close the app and run the newer installer; no uninstall is needed for standard in-place upgrades. -An optional **portable ZIP** is also available for manual extraction. The community installer is currently unsigned, so verify its official source and SHA256; do not treat it as a background auto-updater. See [installation and update guidance](docs/INSTALLATION_AND_UPDATE.md). +An optional [**portable ZIP**](https://github.com/masarray/DigSubAnalyzer/releases/download/v1.4.2/ProcessBusInsight-v1.4.2-win-x64-portable.zip) is also available for manual extraction. Compare its checksum or the installer's checksum with [SHA256SUMS.txt](https://github.com/masarray/DigSubAnalyzer/releases/download/v1.4.2/SHA256SUMS.txt). The community installer is currently unsigned, so verify its official source and SHA256; do not treat it as a background auto-updater. See [installation and update guidance](docs/INSTALLATION_AND_UPDATE.md). Current Windows distribution naming: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b25542ea..58312471 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -32,6 +32,14 @@ Or with GitHub CLI: gh repo edit masarray/DigSubAnalyzer --default-branch main --delete-branch-on-merge=true ``` +## Latest stable Windows installer + +The landing page must lead with a **direct installer .exe** link, never an Actions artifact or the portable ZIP. At deployment time, all three primary download links (header, hero, and Download card) have a stable `v1.4.2` asset fallback. `docs/app.js` queries GitHub `/releases/latest` client-side and upgrades the buttons to the latest non-draft, non-prerelease version **only if** its matching `ProcessBusInsight-vX.Y.Z-win-x64-setup.exe` asset exists. Missing API access retains the already-published fallback. + +The portable ZIP, SHA256 checksum, and release notes remain secondary links. `scripts/verify-landing-release.py` checks that the pinned fallback matches `Directory.Build.props`, that structured metadata/README are synchronized and that no primary CTA silently sends engineers to ZIP or the Releases list. The Pages workflow also runs `node --check docs/app.js` before deployment. When publishing a new stable version, update the pinned fallback URLs and version in the repository even though runtime discovery can follow the latest release automatically. + +Latest public stable release: https://github.com/masarray/DigSubAnalyzer/releases/latest + ## Deploy manually After pushing the latest repository content to `main`, run: diff --git a/docs/app.js b/docs/app.js index be559f1b..5a5d8828 100644 --- a/docs/app.js +++ b/docs/app.js @@ -1,3 +1,72 @@ + +// Public release discovery. GitHub /releases/latest excludes prereleases and +// drafts. Every anchor already has a working direct stable EXE fallback in HTML, +// so API outages, offline use, rate limits, and disabled JavaScript never send +// a user to an obsolete "portable ZIP" CTA or a CI artifact. +const releaseApi = 'https://api.github.com/repos/masarray/DigSubAnalyzer/releases/latest'; +const releaseAssetRoot = 'https://github.com/masarray/DigSubAnalyzer/releases/download/'; +const releaseFilename = /^ProcessBusInsight-v\d+\.\d+\.\d+-win-x64-(setup\.exe|portable\.zip)$/; +const checksumFilename = 'SHA256SUMS.txt'; + +function safeReleaseAsset(asset, tag) { + if (!asset || typeof asset.name !== 'string' || typeof asset.browser_download_url !== 'string') return false; + if (!(releaseFilename.test(asset.name) || asset.name === checksumFilename)) return false; + const prefix = `${releaseAssetRoot}${tag}/`; + return asset.browser_download_url === prefix + asset.name; +} + +function setReleaseLinks(kind, url) { + document.querySelectorAll(`[data-release-download="${kind}"]`).forEach((anchor) => { + anchor.href = url; + }); +} + +function applyLatestStableRelease(release) { + if (!release || release.draft || release.prerelease || + !/^v\d+\.\d+\.\d+$/.test(release.tag_name) || !Array.isArray(release.assets)) return; + + const tag = release.tag_name; + const assets = release.assets.filter((asset) => safeReleaseAsset(asset, tag)); + const exe = assets.find((asset) => asset.name === `ProcessBusInsight-${tag}-win-x64-setup.exe`); + if (!exe) return; // Never direct the primary button to ZIP or a nonexistent asset. + const portable = assets.find((asset) => asset.name === `ProcessBusInsight-${tag}-win-x64-portable.zip`); + const checksum = assets.find((asset) => asset.name === checksumFilename); + const releasePage = `https://github.com/masarray/DigSubAnalyzer/releases/tag/${tag}`; + + setReleaseLinks('installer', exe.browser_download_url); + if (portable) setReleaseLinks('portable', portable.browser_download_url); + if (checksum) setReleaseLinks('checksum', checksum.browser_download_url); + document.querySelectorAll('[data-release-page]').forEach((link) => { link.href = releasePage; }); + document.querySelectorAll('[data-release-version]').forEach((label) => { + label.textContent = `Stable ${tag}`; + }); + + // Keep structured download metadata coherent for crawlers that execute JS. + const structured = document.querySelector('script[type="application/ld+json"]'); + if (structured) { + try { + const data = JSON.parse(structured.textContent); + if (data['@type'] === 'SoftwareApplication') { + data.softwareVersion = tag.slice(1); + data.downloadUrl = exe.browser_download_url; + structured.textContent = JSON.stringify(data); + } + } catch { /* Noncritical metadata; keep the static stable fallback. */ } + } +} + +if (document.querySelector('[data-release-download="installer"]')) { + fetch(releaseApi, { + headers: { Accept: 'application/vnd.github+json' }, + cache: 'no-store' + }).then((response) => { + if (!response.ok) throw new Error('GitHub latest release not accessible'); + return response.json(); + }).then(applyLatestStableRelease).catch(() => { + // HTML already points directly to verified v1.4.2 EXE. + }); +} + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const header = document.querySelector('[data-header]'); const progressBar = document.querySelector('.scroll-progress'); diff --git a/docs/index.html b/docs/index.html index 3e80cef7..c0d1be50 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Process Bus Insight — IEC 61850 SV, GOOSE, PTP and SCL Analyzer - + @@ -41,7 +41,7 @@ "license": "https://spdx.org/licenses/GPL-3.0-or-later.html", "isAccessibleForFree": true, "url": "https://masarray.github.io/DigSubAnalyzer/", - "downloadUrl": "https://github.com/masarray/DigSubAnalyzer/releases", + "downloadUrl": "https://github.com/masarray/DigSubAnalyzer/releases/download/v1.4.2/ProcessBusInsight-v1.4.2-win-x64-setup.exe", "codeRepository": "https://github.com/masarray/DigSubAnalyzer", "image": "https://masarray.github.io/DigSubAnalyzer/screenshot/analyzer-overview.webp", "author": {"@type": "Person", "name": "Ari Sulistiono", "url": "https://github.com/masarray"}, @@ -67,20 +67,20 @@
-
visibility GPL-3.0-or-later · Receive-only · Windows

IEC 61850 Process Bus visibility for engineers who need defensible answers.

Analyze live Sampled Values, GOOSE, PTP timing context, and SCL expected-vs-observed evidence. Reproduce sanitized classic PCAP captures through the same decoder path while keeping timing and evidence limitations visible.

desktop_windows Windows 10/11replay PCAP replayshield No transmit path
+
visibility GPL-3.0-or-later · Receive-only · Windows

IEC 61850 Process Bus visibility for engineers who need defensible answers.

Analyze live Sampled Values, GOOSE, PTP timing context, and SCL expected-vs-observed evidence. Reproduce sanitized classic PCAP captures through the same decoder path while keeping timing and evidence limitations visible.

Stable v1.4.2 · Windows 10/11 x64 · Free and open source

desktop_windows Windows 10/11replay PCAP replayshield No transmit path
hub Process Bus Insight
Analyzer ready
Process Bus Insight dashboard with waveform, phasor, metering, and stream diagnosticsfullscreen Preview
-

Portable engineering software with current legal notices included.

Use an authorized observation path. Npcap is installed separately only when live raw Ethernet capture is required.

+

Get the Windows installer. Start inspecting.

Stable v1.4.2 · self-contained Windows x64. Use the standard installer for in-place update and repair. Npcap is installed separately for live capture.

From packet observation to scoped engineering evidence.

The application shortens the path from traffic to a reviewable finding without treating software capture as certified metrology.

monitoring

Sampled Values

Stream identity, APPID, svID, VLAN, continuity, waveform, RMS, phasor, and sequence context.

-
lan

GOOSE inspection

Publisher state, stNum, sqNum, dataset values, and event changes.

+
lan

GOOSE inspection

Stable multi-source selection, event timeline and Follow live, unread-change dots, unified acknowledgement, and decoded signal change tooltips.

schedule

PTP context

Timing traffic and freshness with explicit timestamp-source limits.

account_tree

SCL comparison

Compare observed traffic with SCD, ICD, or CID expectations.

replay

PCAP replay

Reproduce sanitized classic Ethernet captures through the live decoder path.

@@ -95,7 +95,7 @@
- + diff --git a/docs/styles.css b/docs/styles.css index 3026ee6d..a62fb572 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -308,6 +308,23 @@ h3 { line-height: 1.76; } .hero-actions { margin-top: 30px; } +.hero-release-line { + display: flex; + align-items: center; + gap: 7px; + margin: 12px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.5; +} +.release-live-dot { + flex: none; + width: 7px; + height: 7px; + border-radius: 50%; + background: #16a67d; + box-shadow: 0 0 0 3px rgba(22, 166, 125, 0.09); +} .quick-facts { display: flex; flex-wrap: wrap; @@ -437,6 +454,31 @@ h3 { .site-footer p, .shot-card figcaption span { color: var(--muted); } .download-panel > div > p { margin-top: 14px; max-width: 520px; } +.download-alternatives { + display: flex; + align-items: center; + flex-wrap: wrap; + column-gap: 17px; + row-gap: 9px; + margin-top: 18px; + font-size: 12px; +} +.download-alternatives a { + color: #1761b2; + text-decoration: underline; + text-underline-offset: 3px; +} +.download-alternatives a:hover, +.download-alternatives a:focus-visible { color: #0b417f; } +.download-installer-card { + border-color: rgba(22, 104, 232, 0.27); + background: rgba(234, 244, 255, 0.75); +} +.download-installer-card:hover, +.download-installer-card:focus-visible { + border-color: rgba(22, 104, 232, 0.58); + background: rgba(231, 243, 255, 0.95); +} .download-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } .step-card { display: grid; diff --git a/scripts/verify-landing-release.py b/scripts/verify-landing-release.py new file mode 100644 index 00000000..56dce0c0 --- /dev/null +++ b/scripts/verify-landing-release.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Static release-CTA safety contract (stdlib only). + +The homepage MUST always offer a working, direct stable Windows setup EXE +without needing JavaScript. app.js may upgrade this fallback to the newest +public GitHub Release when the GitHub API is available in a browser. +""" +from __future__ import annotations + +from html.parser import HTMLParser +from pathlib import Path +import json +import re +import xml.etree.ElementTree as ET + +ROOT = Path(__file__).resolve().parents[1] +SITE = ROOT / "docs" / "index.html" +INSTALLER_ROOT = "https://github.com/masarray/DigSubAnalyzer/releases/download/" +version = ET.parse(ROOT / "Directory.Build.props").getroot().find("./PropertyGroup/VersionPrefix") +assert version is not None and version.text, "Release VersionPrefix missing" +tag = "v" + version.text.strip() +fallback = f"{INSTALLER_ROOT}{tag}/ProcessBusInsight-{tag}-win-x64-setup.exe" + +class Links(HTMLParser): + def __init__(self): + super().__init__(convert_charrefs=True) + self.anchors: list[dict[str, str]] = [] + + def handle_starttag(self, name, attrs): + if name == "a": + self.anchors.append(dict(attrs)) + +content = SITE.read_text(encoding="utf-8") +parser = Links() +parser.feed(content) +installer = [a for a in parser.anchors if a.get("data-release-download") == "installer"] +assert len(installer) >= 3, "Header, hero, and download-card must link to EXE" +assert all(a.get("href") == fallback for a in installer), "Every primary CTA must directly download the stable EXE" +assert not any("portable" in a.get("href", "").lower() for a in installer), "Installer CTA must never link to portable ZIP" +assert not any(a.get("href", "").endswith("/releases") for a in installer), "Installer CTA must not open a releases list" +assert "Download installer (.exe)" in content and "Download portable ZIP" not in content +assert all(a.get("href", "").startswith(INSTALLER_ROOT + tag + "/") for a in parser.anchors + if a.get("data-release-download") in {"portable", "checksum"}), "Secondary downloads must match version" + +scripts = re.findall(r'', content, flags=re.S) +assert scripts, "Missing structured app metadata" +metadata = json.loads(scripts[0]) +assert metadata["softwareVersion"] == version.text.strip(), "Structured software version is stale" +assert metadata["downloadUrl"] == fallback, "Structured download URL must be the setup EXE" + +js = (ROOT / "docs" / "app.js").read_text(encoding="utf-8") +for marker in ("releases/latest", "applyLatestStableRelease", "safeReleaseAsset", + "setReleaseLinks('installer'", "release.prerelease", "release.draft"): + assert marker in js, f"Dynamic latest stable EXE resolver missing: {marker}" +assert "data-release-version" in content, "Missing live stable version label" +css = (ROOT / "docs" / "styles.css").read_text(encoding="utf-8") +assert ".download-alternatives" in css and ".download-installer-card" in css +readme = (ROOT / "README.md").read_text(encoding="utf-8") +assert fallback in readme, "README stable installer link is stale" +print(f"Landing installer download contract: PASS ({tag}, 3+ direct EXE CTAs, API fallback)")