diff --git a/.github/labview-ci/catalog.json b/.github/labview-ci/catalog.json
index 78507a40..a5c90e88 100644
--- a/.github/labview-ci/catalog.json
+++ b/.github/labview-ci/catalog.json
@@ -4496,6 +4496,12 @@
".github/actions/publish-gh-pages/action.yml"
]
},
+ "retiredFiles": {
+ "comment": "Tooling files the source has retired. Installs and updates delete them from a target repository, so a workflow the tooling no longer ships cannot keep firing (and failing) in a client repo forever. Only ever list paths this repository no longer contains.",
+ "files": [
+ ".github/workflows/build-toimages-image.yml"
+ ]
+ },
"capabilities": [
{
"id": "dashboard",
@@ -4698,7 +4704,8 @@
"name": "VI Snapshots 2.0",
"summary": "Cross-platform, position-aware VI Browser 2.0 rendering. A vendored Go engine renders each VI's block diagram in place and publishes frames JSON on Windows and Linux, so the VI Browser can show an interactive, zoomable diagram beside the classic gallery.",
"status": "stable",
- "recommended": false,
+ "recommended": true,
+ "required": true,
"supportsOs": [
"windows",
"linux"
@@ -4725,7 +4732,7 @@
".github/workflows/vi-snapshots-json.yml"
]
},
- "notes": "Cross-platform position-aware renderer. The Linux path runs the render engine that is BAKED INTO the Linux worker image (no separate build -- it is copied with the worker); the Windows path runs the same Go engine in the stock NI Windows container (unless EXPERIMENTAL_WINDOWS_IMAGE is set). Renders publish .json (Linux) and .windows.json (Windows) beside the gallery."
+ "notes": "Always installed - every repository gets VI Browser 2.0, so the viewer's 2.0 tab always has a workflow to run. Cross-platform position-aware renderer. The Linux path runs the render engine that is BAKED INTO the Linux worker image (no separate build -- it is copied with the worker); the Windows path runs the same Go engine in the stock NI Windows container (unless EXPERIMENTAL_WINDOWS_IMAGE is set). Renders publish .json (Linux) and .windows.json (Windows) beside the gallery."
},
{
"id": "unit-tests",
diff --git a/.github/labview-ci/config.example.yml b/.github/labview-ci/config.example.yml
index 53555cb3..7acf2ed7 100644
--- a/.github/labview-ci/config.example.yml
+++ b/.github/labview-ci/config.example.yml
@@ -80,11 +80,14 @@ config:
vidiff: base
# Installed capabilities (see .github/labview-ci/catalog.json for the full list).
+# Capabilities the catalog marks `required` are installed in every repository and
+# are added back on the next install or update even if you delete them here.
activities:
- dashboard
- masscompile
- vi-analyzer
- vidiff
+ - snapshots-2
# ----------------------------------------------------------------------------
# GitHub Actions variables the GitHub workflows honor at runtime (set with
diff --git a/.github/labview-ci/install.py b/.github/labview-ci/install.py
index e15a72e0..9de9ca55 100644
--- a/.github/labview-ci/install.py
+++ b/.github/labview-ci/install.py
@@ -205,18 +205,41 @@ def build_substitutions(catalog: dict, owner: str | None, name: str | None,
return subs
-def default_activities(catalog: dict) -> list[str]:
+def required_activities(catalog: dict) -> list[str]:
+ """Capabilities every installation gets, whatever the caller asked for.
+
+ A capability flagged `required` in the catalog is not a choice: VI Snapshots
+ 2.0 is one, because the VI Browser's 2.0 tab is always visible and needs its
+ render workflow to exist in the repository. Without this, a repo installed
+ before the capability existed never gained it -- updates reuse the manifest's
+ activity list, so the omission is self-perpetuating and the viewer's Run
+ button 404s forever on a workflow that was never vendored.
+ """
return [
+ c["id"] for c in catalog.get("capabilities", [])
+ if c.get("required") and c.get("status", "stable") not in DEFAULT_EXCLUDED_STATUSES
+ ]
+
+
+def default_activities(catalog: dict) -> list[str]:
+ defaults = [
c["id"] for c in catalog.get("capabilities", [])
if c.get("recommended") and c.get("status", "stable") not in DEFAULT_EXCLUDED_STATUSES
]
+ return defaults + [a for a in required_activities(catalog) if a not in defaults]
def resolve_activities(catalog: dict, activities: list[str]) -> list[str]:
- """Expand hard capability dependencies, preserving the requested order."""
+ """Expand hard capability dependencies, preserving the requested order.
+
+ Required capabilities are appended to whatever was requested, so an older
+ manifest (or an explicit --activities list that predates them) still heals on
+ the next install or update.
+ """
by_id = {c["id"]: c for c in catalog.get("capabilities", [])}
selected: list[str] = []
stack = list(activities)
+ stack.extend(a for a in required_activities(catalog) if a not in stack)
while stack:
cid = stack.pop(0)
if cid in selected:
@@ -611,6 +634,35 @@ def gitlab_legacy_dashboard_builder(path: Path) -> bool:
)
+def prune_retired_files(catalog: dict, target_root: Path, keep: set[str],
+ dry_run: bool, stats: dict) -> None:
+ """Delete tooling files this repository no longer ships.
+
+ A retired workflow keeps firing in a client repo long after the source has
+ dropped it -- build-toimages-image.yml outlived the render engine it built
+ and failed on every push. The configurator's stale-file scan only recognises
+ files by content signature, so anything that never mentioned the tooling by
+ name survived every reinstall. This list is the explicit backstop.
+ """
+ for rel in catalog.get("retiredFiles", {}).get("files", []):
+ if rel in keep:
+ warn(f"retired file {rel} is still installed by the catalog - skipping.")
+ continue
+ target = target_root / rel
+ if not target.is_file():
+ continue
+ if dry_run:
+ stats["planned"] += 1
+ log(f" would prune {rel} (retired)")
+ continue
+ try:
+ target.unlink()
+ stats["pruned"] += 1
+ log(f" prune (retired) {rel}")
+ except OSError as exc:
+ warn(f"could not prune {rel}: {exc}")
+
+
def prune_gitlab_provider_files(target_root: Path, provider: dict, dry_run: bool,
stats: dict) -> None:
"""Remove only obsolete files which a previous provider package owned."""
@@ -1162,6 +1214,7 @@ def main() -> int:
for entry in file_list:
copy_entry(entry, source_root, target_root, subs, force, args.dry_run, stats,
preserve, update, stamp_version)
+ prune_retired_files(catalog, target_root, set(file_list), args.dry_run, stats)
# The dashboard generator (actions/dashboard) is a local path that only exists
# in the tooling repo and can't be rebranded into a consumer, so the vendored
@@ -1200,7 +1253,8 @@ def main() -> int:
add_paths = ".github .gitlab .gitlab-ci.yml" if provider == "gitlab" else ".github"
log(f" 2. Commit the update: git add {add_paths} && git commit -m \"Update LabVIEW CI\" && git push")
return 0
- log(f"Installed {stats['installed']} file(s); {stats['skipped']} skipped (already present).")
+ pruned = f"; {stats['pruned']} retired file(s) removed" if stats["pruned"] else ""
+ log(f"Installed {stats['installed']} file(s); {stats['skipped']} skipped (already present){pruned}.")
if stats["skipped"]:
log("Use --force to overwrite skipped files.")
if provider == "gitlab":
diff --git a/.github/labview-ci/notes/snapshots-2-always-installed.md b/.github/labview-ci/notes/snapshots-2-always-installed.md
new file mode 100644
index 00000000..ab68d980
--- /dev/null
+++ b/.github/labview-ci/notes/snapshots-2-always-installed.md
@@ -0,0 +1,11 @@
+VI Snapshots 2.0 is now installed in every repository and can no longer be turned
+off. The VI Browser always offers a 2.0 view, so a repository that skipped the
+capability had no `vi-snapshots-json.yml` / `vi-snapshots-json-windows.yml` to run
+and the viewer's "Run the 2.0 snapshot" button failed with a misleading token
+error. The omission also perpetuated itself: reinstalls and updates reuse the
+activity list recorded in `.github/labview-ci.yml`, so a repository set up before
+the capability existed never gained it. Installs and updates now add it back
+automatically, the configurator shows it as "Always installed" rather than an
+option, and the retired `build-toimages-image.yml` workflow — which outlived the
+render engine it built and failed on every push — is deleted from repositories
+that still carry it.
diff --git a/.github/labview/validate-catalog-source-sync.py b/.github/labview/validate-catalog-source-sync.py
index 975a12ed..21eddb80 100644
--- a/.github/labview/validate-catalog-source-sync.py
+++ b/.github/labview/validate-catalog-source-sync.py
@@ -363,6 +363,40 @@ def main() -> int:
if obsolete:
failures.append(f"custom-image still vendors obsolete worker files: {obsolete!r}")
+ # A `required` capability is installed in every repository, so it must be
+ # something a client can actually receive: shipping one that is planned, or
+ # flagged defaultOff, would install a contradiction on every consumer.
+ required_caps = [cap for cap in capabilities if cap.get("required")]
+ if not required_caps:
+ failures.append(
+ "no capability is marked required. VI Snapshots 2.0 is installed "
+ "everywhere so the VI Browser's 2.0 tab always has a workflow to run; "
+ "dropping the flag silently makes it optional again."
+ )
+ for capability in required_caps:
+ capability_id = capability.get("id", "")
+ if capability.get("status") in ("planned", "experimental"):
+ failures.append(
+ f"capability {capability_id!r} is required but {capability.get('status')!r}"
+ )
+ if capability.get("defaultOff"):
+ failures.append(f"capability {capability_id!r} is both required and defaultOff")
+
+ # Retired files are deleted from consumers on install/update, so a path here
+ # must be gone from the source and installed by nothing.
+ installable = set((catalog.get("base") or {}).get("files") or [])
+ for capability in capabilities:
+ for relpaths in (capability.get("files") or {}).values():
+ installable.update(relpaths or [])
+ for relpath in (catalog.get("retiredFiles") or {}).get("files") or []:
+ if path_exists(relpath):
+ failures.append(
+ f"retired file still exists in the source: {relpath}. Installs delete "
+ "it from every consumer, so it cannot also be a file this repo ships."
+ )
+ if relpath in installable:
+ failures.append(f"retired file is still installed by the catalog: {relpath}")
+
for capability in capabilities:
capability_id = capability.get("id", "")
files = capability.get("files") or {}
diff --git a/.github/pages/configure.html b/.github/pages/configure.html
index b4ef002a..22194bdb 100644
--- a/.github/pages/configure.html
+++ b/.github/pages/configure.html
@@ -460,6 +460,13 @@ Save and apply
// never gates config.os).
const OS_COLS = [["linux", "Linux"], ["windows", "Windows"]];
function capById(id) { return (catalog.capabilities || []).find(c => c.id === id); }
+ // Capabilities the catalog marks `required` are installed in every repository
+ // (VI Snapshots 2.0 is one), so they are added to the selection even when the
+ // manifest this page loaded predates them.
+ function requiredIds() {
+ return (catalog.capabilities || []).filter(c => c.required && c.status !== "planned").map(c => c.id);
+ }
+ function addRequiredActs() { requiredIds().forEach(id => actSel.add(id)); }
function defaultOsFor(cap) {
const sup = cap.supportsOs || [];
const within = sup.filter(o => osPref.has(o));
@@ -1375,6 +1382,7 @@ Save and apply
if (m.version) renderVersions(m.version);
osPref.clear(); (m.os.length ? m.os : ["windows"]).forEach(o => osPref.add(o));
if (m.activities.length) { actSel.clear(); m.activities.forEach(a => actSel.add(a)); }
+ addRequiredActs();
actionOs.clear();
actSel.forEach(id => {
const cap = capById(id); if (!cap) return;
@@ -1471,6 +1479,7 @@ Save and apply
// Pre-select recommended activities; seed each one's platforms from the
// default OS preference; load the default dependency manifest.
catalog.capabilities.filter(c => c.recommended && c.status !== "planned").forEach(c => actSel.add(c.id));
+ addRequiredActs();
seedActionOs(); recomputeOsSel();
renderVersions(); renderActivities();
pkgObj = await loadDefaultPackages();
diff --git a/.github/pages/documentation.html b/.github/pages/documentation.html
index 3c53ffed..21992fd0 100644
--- a/.github/pages/documentation.html
+++ b/.github/pages/documentation.html
@@ -401,7 +401,7 @@ 5. The activities & when to use each | Unit Tests | Runs Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report. | Win |
| Antidoc | Generates project documentation from the VI hierarchy using Wovalab's Antidoc CLI. | Win |
| VI Snapshots / Browser | Renders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser). | Win |
- | VI Snapshots 2.0 | Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. | Win Linux |
+ | VI Snapshots 2.0 | Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. Installed in every repository — the VI Browser always offers a 2.0 view, so its render workflow is never optional. | Win Linux |
| Dashboard | Aggregates everything above into a single status page on GitHub Pages. | runner host |
@@ -1885,7 +1885,7 @@ Phase 1 — HTML snapshots and Windows 2.0 frames Win
per-commit manifest.json (VI → by-blob HTML), the rolling commits.json, and the
Windows JSON index used by the in-place browser.
Phase 2 — position-aware JSON (VI Snapshots 2.0) Windows Linux
- Phase 2 is the separate VI Snapshots 2.0 capability, selectable on Windows and Linux (the classic gallery above is Windows-only). The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render
+
Phase 2 is the separate VI Snapshots 2.0 capability. It is installed in every repository on both Windows and Linux and cannot be turned off (the classic gallery above is Windows-only and remains optional), because the VI Browser always shows a 2.0 tab and needs a render workflow behind it. The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render
position-aware frame JSON for the in-place browser. The render engine is baked into the Linux worker image (and runs in the stock NI image on Windows) — a Go batch runner
(main.go) reads the same TSV worklist and shells out to lvctl toimages — a native
Go VI-Server TCP client that launches LabVIEW under Xvfb, captures the front panel and block diagram as
diff --git a/.github/pages/integrate.html b/.github/pages/integrate.html
index 92b89b04..82e91918 100644
--- a/.github/pages/integrate.html
+++ b/.github/pages/integrate.html
@@ -76,6 +76,7 @@
.badge.adv{color:#9a6700;border-color:#9a6700}
.badge.planned{color:#bf8700;border-color:#bf8700}
.badge.exp{color:#cf222e;border-color:#cf222e}
+ .badge.always{color:#fff;background:var(--fg-muted);border-color:transparent}
.badge.os{color:var(--fg-muted)}
.note{font-size:.78em;color:var(--fg-muted);margin-top:6px}
.exp-details{margin-top:8px;font-size:.84em;color:var(--fg-muted)}
@@ -588,6 +589,11 @@
4. Finish setup
}
function isPlanned(cap) { return cap && cap.status === "planned"; }
+ // Capabilities the catalog marks `required` are not a choice. VI Snapshots 2.0
+ // is one: the VI Browser always shows a 2.0 tab, so every repository needs its
+ // render workflow. They are checked, locked, and force-added to a kept manifest.
+ function isRequired(cap) { return !!(cap && cap.required && !isPlanned(cap)); }
+ function requiredIds() { return (catalog.capabilities || []).filter(isRequired).map(c => c.id); }
function renderCaps() {
const host = $("caps");
@@ -598,10 +604,13 @@ 4. Finish setup
const wrap = document.createElement("label");
wrap.className = "cap" + (planned ? " disabled" : "");
const cb = document.createElement("input");
- cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned;
+ const required = isRequired(cap);
+ cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned || required;
// Capabilities are on by default unless the catalog flags one defaultOff
// (opt-in), like Antidoc, which needs the shared image + network at run time.
- cb.checked = !planned && !cap.defaultOff;
+ // A `required` capability cannot be turned off at all.
+ cb.checked = required || (!planned && !cap.defaultOff);
+ if (required) cb.title = "Always installed \u2014 this action cannot be turned off.";
if (cb.checked) selected.add(cap.id);
cb.addEventListener("change", () => {
markCapabilityChoicesChanged();
@@ -611,7 +620,8 @@ 4. Finish setup
});
const body = document.createElement("div");
const badges = [];
- if (cap.recommended) badges.push('Recommended');
+ if (required) badges.push('Always installed');
+ else if (cap.recommended) badges.push('Recommended');
if (cap.status === "advanced") badges.push('Advanced');
if (planned) badges.push('Coming soon');
const osBadges = (cap.supportsOs || []).map(o => `${o}`).join("");
@@ -679,7 +689,10 @@ 4. Finish setup
// ---- file resolution (mirrors install.py) ----------------------------
function resolveActivities(activities) {
const map = {}; catalog.capabilities.forEach(c => map[c.id] = c);
+ // Mirrors install.py: hard requires are expanded, and required capabilities
+ // are appended whatever the caller asked for.
const chosen = []; const stack = [...activities];
+ requiredIds().forEach(id => { if (!stack.includes(id)) stack.push(id); });
while (stack.length) {
const id = stack.shift();
if (chosen.includes(id)) continue;
@@ -2605,6 +2618,9 @@ 4. Finish setup
// locked to the supported year (see renderVersions), so reinstalling a repo
// that was pinned to an unsupported year heals it instead of re-breaking.
}
+ // A required capability is added even when the prior manifest predates it —
+ // otherwise "keep current settings" carries the omission forward forever.
+ requiredIds().forEach(id => { if (!activities.includes(id)) activities.push(id); });
return { activities, osList, version };
}
diff --git a/.github/workflows/reconfigure.yml b/.github/workflows/reconfigure.yml
index 91ff680e..e0190a1b 100644
--- a/.github/workflows/reconfigure.yml
+++ b/.github/workflows/reconfigure.yml
@@ -260,6 +260,18 @@ jobs:
else (['windows', 'linux'] if oss == 'both' else [oss])
acts = [a.strip() for a in acts_in.split(',') if a.strip()] \
or prev['activities'] or ['dashboard', 'masscompile', 'vi-analyzer', 'vidiff']
+ # Capabilities the catalog marks `required` are installed in every
+ # repository (VI Snapshots 2.0), so the manifest must list them even when
+ # it was written before the capability existed -- otherwise the next
+ # tooling update reads this file back and skips them again.
+ try:
+ _cat = json.loads(pathlib.Path('.github/labview-ci/catalog.json').read_text(encoding='utf-8'))
+ for _cap in _cat.get('capabilities', []):
+ if _cap.get('required') and _cap.get('status') not in ('planned', 'experimental') \
+ and _cap.get('id') not in acts:
+ acts.append(_cap['id'])
+ except Exception:
+ pass
now = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
max_parallel = int(runners) if runners.isdigit() and int(runners) > 0 else (prev.get('maxParallel') or 5)
diff --git a/actions/dashboard/configure.html b/actions/dashboard/configure.html
index 1998ba36..019a6f0a 100644
--- a/actions/dashboard/configure.html
+++ b/actions/dashboard/configure.html
@@ -460,6 +460,13 @@ Save and apply
// never gates config.os).
const OS_COLS = [["linux", "Linux"], ["windows", "Windows"]];
function capById(id) { return (catalog.capabilities || []).find(c => c.id === id); }
+ // Capabilities the catalog marks `required` are installed in every repository
+ // (VI Snapshots 2.0 is one), so they are added to the selection even when the
+ // manifest this page loaded predates them.
+ function requiredIds() {
+ return (catalog.capabilities || []).filter(c => c.required && c.status !== "planned").map(c => c.id);
+ }
+ function addRequiredActs() { requiredIds().forEach(id => actSel.add(id)); }
function defaultOsFor(cap) {
const sup = cap.supportsOs || [];
const within = sup.filter(o => osPref.has(o));
@@ -1396,6 +1403,7 @@ Save and apply
if (m.version) renderVersions(m.version);
osPref.clear(); (m.os.length ? m.os : ["windows"]).forEach(o => osPref.add(o));
if (m.activities.length) { actSel.clear(); m.activities.forEach(a => actSel.add(a)); }
+ addRequiredActs();
actionOs.clear();
actSel.forEach(id => {
const cap = capById(id); if (!cap) return;
@@ -1492,6 +1500,7 @@ Save and apply
// Pre-select recommended activities; seed each one's platforms from the
// default OS preference; load the default dependency manifest.
catalog.capabilities.filter(c => c.recommended && c.status !== "planned").forEach(c => actSel.add(c.id));
+ addRequiredActs();
seedActionOs(); recomputeOsSel();
renderVersions(); renderActivities();
pkgObj = await loadDefaultPackages();
diff --git a/actions/dashboard/documentation.html b/actions/dashboard/documentation.html
index 3c53ffed..21992fd0 100644
--- a/actions/dashboard/documentation.html
+++ b/actions/dashboard/documentation.html
@@ -401,7 +401,7 @@ 5. The activities & when to use each | Unit Tests | Runs Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report. | Win |
| Antidoc | Generates project documentation from the VI hierarchy using Wovalab's Antidoc CLI. | Win |
| VI Snapshots / Browser | Renders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser). | Win |
- | VI Snapshots 2.0 | Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. | Win Linux |
+ | VI Snapshots 2.0 | Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. Installed in every repository — the VI Browser always offers a 2.0 view, so its render workflow is never optional. | Win Linux |
| Dashboard | Aggregates everything above into a single status page on GitHub Pages. | runner host |
@@ -1885,7 +1885,7 @@ Phase 1 — HTML snapshots and Windows 2.0 frames Win
per-commit manifest.json (VI → by-blob HTML), the rolling commits.json, and the
Windows JSON index used by the in-place browser.
Phase 2 — position-aware JSON (VI Snapshots 2.0) Windows Linux
- Phase 2 is the separate VI Snapshots 2.0 capability, selectable on Windows and Linux (the classic gallery above is Windows-only). The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render
+
Phase 2 is the separate VI Snapshots 2.0 capability. It is installed in every repository on both Windows and Linux and cannot be turned off (the classic gallery above is Windows-only and remains optional), because the VI Browser always shows a 2.0 tab and needs a render workflow behind it. The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render
position-aware frame JSON for the in-place browser. The render engine is baked into the Linux worker image (and runs in the stock NI image on Windows) — a Go batch runner
(main.go) reads the same TSV worklist and shells out to lvctl toimages — a native
Go VI-Server TCP client that launches LabVIEW under Xvfb, captures the front panel and block diagram as
diff --git a/actions/dashboard/integrate.html b/actions/dashboard/integrate.html
index 92b89b04..82e91918 100644
--- a/actions/dashboard/integrate.html
+++ b/actions/dashboard/integrate.html
@@ -76,6 +76,7 @@
.badge.adv{color:#9a6700;border-color:#9a6700}
.badge.planned{color:#bf8700;border-color:#bf8700}
.badge.exp{color:#cf222e;border-color:#cf222e}
+ .badge.always{color:#fff;background:var(--fg-muted);border-color:transparent}
.badge.os{color:var(--fg-muted)}
.note{font-size:.78em;color:var(--fg-muted);margin-top:6px}
.exp-details{margin-top:8px;font-size:.84em;color:var(--fg-muted)}
@@ -588,6 +589,11 @@
4. Finish setup
}
function isPlanned(cap) { return cap && cap.status === "planned"; }
+ // Capabilities the catalog marks `required` are not a choice. VI Snapshots 2.0
+ // is one: the VI Browser always shows a 2.0 tab, so every repository needs its
+ // render workflow. They are checked, locked, and force-added to a kept manifest.
+ function isRequired(cap) { return !!(cap && cap.required && !isPlanned(cap)); }
+ function requiredIds() { return (catalog.capabilities || []).filter(isRequired).map(c => c.id); }
function renderCaps() {
const host = $("caps");
@@ -598,10 +604,13 @@ 4. Finish setup
const wrap = document.createElement("label");
wrap.className = "cap" + (planned ? " disabled" : "");
const cb = document.createElement("input");
- cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned;
+ const required = isRequired(cap);
+ cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned || required;
// Capabilities are on by default unless the catalog flags one defaultOff
// (opt-in), like Antidoc, which needs the shared image + network at run time.
- cb.checked = !planned && !cap.defaultOff;
+ // A `required` capability cannot be turned off at all.
+ cb.checked = required || (!planned && !cap.defaultOff);
+ if (required) cb.title = "Always installed \u2014 this action cannot be turned off.";
if (cb.checked) selected.add(cap.id);
cb.addEventListener("change", () => {
markCapabilityChoicesChanged();
@@ -611,7 +620,8 @@ 4. Finish setup
});
const body = document.createElement("div");
const badges = [];
- if (cap.recommended) badges.push('Recommended');
+ if (required) badges.push('Always installed');
+ else if (cap.recommended) badges.push('Recommended');
if (cap.status === "advanced") badges.push('Advanced');
if (planned) badges.push('Coming soon');
const osBadges = (cap.supportsOs || []).map(o => `${o}`).join("");
@@ -679,7 +689,10 @@ 4. Finish setup
// ---- file resolution (mirrors install.py) ----------------------------
function resolveActivities(activities) {
const map = {}; catalog.capabilities.forEach(c => map[c.id] = c);
+ // Mirrors install.py: hard requires are expanded, and required capabilities
+ // are appended whatever the caller asked for.
const chosen = []; const stack = [...activities];
+ requiredIds().forEach(id => { if (!stack.includes(id)) stack.push(id); });
while (stack.length) {
const id = stack.shift();
if (chosen.includes(id)) continue;
@@ -2605,6 +2618,9 @@ 4. Finish setup
// locked to the supported year (see renderVersions), so reinstalling a repo
// that was pinned to an unsupported year heals it instead of re-breaking.
}
+ // A required capability is added even when the prior manifest predates it —
+ // otherwise "keep current settings" carries the omission forward forever.
+ requiredIds().forEach(id => { if (!activities.includes(id)) activities.push(id); });
return { activities, osList, version };
}