The repo-connect provider strip was a static "explainer" — clicking GitLab/
Bitbucket/Gitea did nothing, and the GitHub picker showed regardless, which
read as confusing. Now (when a host form passes onSelect) the chips are radio
buttons that drive the connect method: GitHub shows the one-click picker;
every other provider shows the paste-a-URL field with that host's placeholder
(gitlab.com / bitbucket.org / gitea / scp-style ssh). A recognized URL still
auto-selects its provider. GIT_PROVIDERS gained `oauth` + `placeholder`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Overview: group cards are fully clickable and navigate to Messages
- Overview: add 'system' badge for system-owned groups
- Groups: add per-row 'Create Queue' action and clearer 'View Queues' label
- Messages: remove out-of-place 'Create Queue' button; keep Send Message only
- Create Queue modal: include group selector
- Add pluggable queue bus with SQLAlchemy backend (Redis-ready interface)
- Models: QueueGroup, Queue, QueueMessage with retries, visibility timeout,
delayed delivery, priority ordering, and dead-letter handling
- REST API at /api/v1/queue for groups, queues, messages, and stats
- Plugin SDK: from app.plugins_sdk import queue
- Operations UI page with group/queue/message management and send-message form
- Migrate webhook deliveries from raw threading.Thread to the queue bus
- Add webhook consumer with retry/dead-letter support
- Migration 022_queue_bus and backend tests
Add a shared GithubRepoPicker that lists the user's repositories from the
GitHub source-connection OAuth that already powers /services/new (no new
backend), and render it above the paste-a-URL fields in RepoConnectForm.
Picking a repo fills the clone URL + default branch, so linking a repo to a
WordPress site — and any future RepoConnectForm surface — becomes "authorize
once, pick a repo" instead of pasting a URL. Falls back gracefully to the URL
form when GitHub isn't configured/connected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update .gitignore to allow committed screenshot assets under docs/screenshots/ (these are generated mock-data README images and should be tracked). Fix a bug in frontend/src/pages/ServiceDetail.jsx: avoid referencing isUpload from outer scope (temporal dead zone) by deriving it inside the useEffect, and replace the effect dependency with service?.source so the effect runs correctly when the service source changes. This prevents a render-time crash and ensures versions are loaded for uploaded services.
- Extract the repo-connect form into components/git/RepoConnectForm.jsx
(provider strip + URL/branch/auto-deploy + optional tracked paths +
connected state) — the canonical reusable surface. components/wordpress/
GitConnectForm.jsx is now a thin WordPress-flavored wrapper over it, so
WordPress renders identically and other pages can drop the shared form in.
- Make the WordPress commit log clickable: each SHA links to the commit on
the remote (GitHub/GitLab /commit, Bitbucket /commits; https + scp-ssh).
- Fix off-center icon tiles on /services/new (grid + place-items + block svg
instead of inline-flex).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework /wordpress/:id to match the new prototype and stay consistent with
the rest of the app:
- Top bar is the canonical PageTopbar (.sk-topbar) — same chrome as the
WordPress list page; breadcrumb in the title slot, trimmed actions
(Open Files, Open Database, View Logs, Dashboard, Auto Login).
- Identity row: no card background + empty-circle icon (per the prototype),
in a CENTERED max-width body while the top bar stays full-bleed
(.main-content:has(.wp-detail-page){padding:0}, normal scroll kept).
- Overview leads with a non-redundant KPI row (uptime / visitors / error
rate / disk), consolidated Site information, a Traffic chart, and a
Recent-activity feed; redundant panels dropped (killed the version-3x).
- Settings: grouped, URL-routed left nav (/wordpress/:id/settings/:section,
shareable + refresh-safe) — General (Add Domain / Change URL / Clone),
Configuration (PHP, Updates), Connections (Git), Security (Security,
Vulnerabilities), Reports; those tabs left the top strip.
- Fix a wasted right column: .app-overview-grid collapses to one column
when it has no .app-overview-right.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remote Access was a standalone Infrastructure sidebar item, but it's
fundamentally agent-to-agent WireGuard pairing, so move it into the
Servers tab group (mirrors the Dynamic-DNS-into-Domains consolidation).
The /remote-access route now lives under the Servers TabGroupLayout and
the Servers sidebar item stays lit via matchPrefixes; the URL is
unchanged so existing links still work.
Rebuild the page body: the "Expose a Local Service" action rides the
shared PageTopbar via useTopbarActions, and the shadcn Card/Badge markup
is replaced with page-local .ra-* SCSS (styles/pages/_remote-access.scss)
plus the ds Pill and shared EmptyState. The old page rendered with no top
padding because the unlayered .ui-card-content rule beats the layered
Tailwind p-5 utility; a purpose-built SCSS element sidesteps that.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the sections (Terminal / Log Files / System Journal / Processes / Services) as inline PageTopbar tabs routed via /terminal/<tab>, like the Servers group — dropping the separate shadcn TabsList and the 'shells / logs / processes / services' tagline. Title is now just 'Logs'. The active pane swaps on the URL and keeps filling the full-bleed region.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The api module called this.get/post/patch/delete, which the ApiService doesn't expose (only request(path, {method, body})) — so every load threw 'this.get is not a function'. Rewrote it to use request() like the other api modules. Also dropped an erroneous className='tabs' on the <Tabs> root: that class (display:flex, meant for the TabsList strip) made the root a row, so the panel rendered beside the tabs instead of below them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dynamic DNS becomes a tab in the Domains group (Domains / DNS Zones / SSL / Dynamic DNS) and is removed from the main nav, so DNS lives in one place. GPU Monitor is hidden from the sidebar when the host reports no GPU (getGpuInfo().available), reusing the wpInstalled-style condition gate for top-level items.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terminal/Logs now fills the viewport like the Servers ops view (full-bleed flex layout through the Radix tab panel). LogContent sticks to the newest line on new content/selection (respecting manual scroll-up) and shows a floating LIVE badge while a log auto-follows its tail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vaults and webhook endpoints now carry a nullable workspace_id FK and are filtered by the active workspace context (X-Workspace-Id header or ?workspace_id), mirroring the servers/apps scoping (#33). New rows are stamped with the resolved workspace; with no context the view stays unscoped. Migration 021 adds the columns + indexes and backfills existing rows to the Default workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a TabGroupLayout and useTopbarActions hook to render shared PageTopbar + sub-nav for tabbed page groups (Servers, Domains, Services, Files, Monitoring, Marketplace, WordPress). Pages now publish top-bar actions to the enclosing layout instead of rendering their own PageTopbar, reducing remounts and keeping sidebar highlighting consistent. Sidebar items gain matchPrefixes to stay active across grouped routes. Updated many pages to use the hook and new layout, adjusted classes for full-bleed/tab-group shells, and added supporting SCSS. This makes tab groups behave like real tabs and centralizes header behavior.
The localized READMEs are older, slimmer variants missing several sections the
English README has, so each picked up the feature entries that map to existing
sections (Manual & Zip deploys, Container Lifecycle, SSL/TLS hardening, S3/B2
browsing, Passkeys/WebAuthn, WAF, GPU monitoring) plus the full set of new
completed roadmap items, the Tech Stack security row, and a NEW_FEATURES link.
Features whose parent section doesn't exist in a given translation (Remote
Service Tunnels, Connections, WordPress Publishing, Dynamic DNS, Secrets &
Webhook Gateway, CVE/SBOM, Encrypted Secrets at Rest) are represented in that
file's roadmap rather than fabricating new sections. No Layout Switcher
(UI-only) line added, consistent with the English README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit extended past the unpushed window into already-pushed work (~June 11-18)
that never reached the README. Add the substantive, user-facing features only —
not UI restyles.
- Remote Service Tunnels — expose a private/NAT'd service through an edge server
over an agent-managed WireGuard tunnel (the agent network-tunnel feature).
- Connections — single front door for external accounts (source, cloud, DNS,
registrars with expiry tracking, SMTP relays, S3/B2), credentials encrypted.
- WordPress Publishing — real subdomains, previewable URL-swap, custom domains
with auto-DNS and wildcard HTTPS.
- Manual & Zip Deploys; S3/B2 browsing in the File Manager.
- Roadmap entries for the above.
Also drop the Layout Switcher line (a UI change, not a feature) from the README
and CHANGELOG, per the "meaningful features only" guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dev branch is 23 commits ahead of origin with many user-facing features that
were never reflected in the docs. Bring them current.
- README: add WAF, container CVE/SBOM (grype/syft), passkeys/WebAuthn, dynamic
DNS, GPU monitoring, container lifecycle (image updates / auto-sleep /
auto-scale), secrets + inbound webhook gateway, backup encryption, optional
hardened TLS, encrypted secrets at rest, and the layout switcher to the
Features list, Roadmap, and Tech Stack; link NEW_FEATURES.md.
- CHANGELOG: add the above to [Unreleased] and a new Security subsection
(CVE/SBOM, optional/hardened TLS + auto-CAA + HSTS gating, encryption at rest).
- NEW_FEATURES.md: add §6 documenting the SSL/TLS hardening follow-up
(02afdec..a09bf0f) and renumber Known follow-ups to §7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Flask middleware emitted HSTS with includeSubDomains+preload whenever not in
debug mode -- even on HTTP-only or Cloudflare-Flexible installs, where preload is
a hard-to-reverse commitment. HTTPS is intentionally optional, so HSTS is now
gated on the operator's recorded SSL choice.
- config._resolve_ssl_mode(): SERVERKIT_SSL_MODE env -> /etc/serverkit/ssl-mode
file -> safe 'insecure' default. Exposed as SSL_MODE / HSTS_ENABLED.
- security middleware sends HSTS only when HSTS_ENABLED and not debug. Behind a
proxy Flask can't distinguish real TLS from a Flexible edge via
X-Forwarded-Proto, so we trust the installer's choice, not the request scheme.
- install.sh records SERVERKIT_SSL_MODE in .env; existing installs are covered by
the /etc/serverkit/ssl-mode file. The nginx edge still emits HSTS in its own
secure server block, so secure installs are unaffected.
Proving tests in tests/test_security_headers.py. Full backend suite: 342 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a cert is issued for a domain managed by a connected DNS provider,
ServerKit now auto-creates a `CAA 0 issue "letsencrypt.org"` record at the
zone apex -- pinning issuance to our CA and clearing the "CAA not enabled"
scanner finding. Degrades to manual instructions when no provider manages the
domain; never fails issuance on a CAA hiccup.
- DNSProviderService.ensure_caa_record(): high-level helper mirroring
ensure_a_record (find managing zone -> upsert apex CAA -> degrade to manual).
- parse_caa_value(): BIND 'flags tag "value"' -> Cloudflare/DO data object.
- Cloudflare set_record now sends the structured `data` object CAA requires
(a flat `content` string is rejected), and matches on tag+value so it does
not clobber a different CA's existing CAA record.
- DigitalOcean set_record sends flags/tag for CAA too.
- DNSZoneService._cloudflare_sync uses structured data for CAA, so the preset
CAA record syncs correctly to Cloudflare-backed zones.
- ssl_service.obtain_certificate calls the hook and surfaces the result.
Proving tests in tests/test_dns_caa.py (7 cases, all green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KIMI's optional-SSL commit hardened ssl_protocols/ssl_ciphers per-vhost but
left the system nginx.conf http{} defaults untouched, so the default server
and any non-ServerKit vhost could still negotiate TLS 1.0/1.1 and weak ciphers
(the scan's "insecure TLS versions" / "weak ciphers in TLS 1.2" findings).
- install.sh: harden_global_tls() rewrites (or injects) ssl_protocols and
ssl_ciphers in /etc/nginx/nginx.conf to TLS1.2/1.3 + AEAD-only. Editing the
line in place avoids nginx's duplicate-directive error on Debian/Ubuntu,
which already set ssl_protocols in http{}.
- update.sh: re-applies the floor so existing installs harden on upgrade too.
- Persist the panel domain to /etc/serverkit/panel-domain; update.sh recovers
it from there (or the live cert path) instead of the commented-out .env
SERVERKIT_PUBLIC_URL grep, which never matched -> secure installs were
getting the YOUR_DOMAIN placeholder re-applied and nginx -t failing on update.
- update.sh now degrades to the HTTP config when a secure cert is missing,
rather than leaving nginx unable to reload.
Global TLS settings are inert in HTTP-only mode, so optional SSL is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add secure nginx config with TLS 1.2+, HSTS preload, CSP, and security headers
- Add insecure HTTP fallback config for users who cannot or do not want HTTPS
- Make installer best-effort SSL: try Let's Encrypt, fall back to HTTP without failing
- Preserve secure/insecure SSL choice across updates via /etc/serverkit/ssl-mode
- Support SERVERKIT_SKIP_SSL=1 to disable HTTPS entirely
- Add CAA record to default DNS zone template
- Harden generated per-app nginx configs with HSTS preload, CSP, and security headers
- Align Flask security middleware headers with nginx edge config
Reviewer-facing summary of the new endpoints, pages, models, config flags,
and operational notes for the DDNS / image-update / auto-sleep / auto-scale /
GPU / backup-encryption / WAF features and the layout switcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API client: WAF methods (getWafPolicy/updateWafPolicy/applyWaf/
getWafEvents/getWafStatus/installWaf) in containerOps.js.
- WafPanel.jsx: install banner (+ install action), mode (off/detect/block)
with descriptions, paranoia level + anomaly threshold, a disabled-CRS-rule
chip editor, Save (saves + applies) + Re-apply (surfaces manual_include
when no vhost is found), and a recent-events table.
- ApplicationDetail: a "WAF" tab (nginx-served apps), wired like Container Ops.
Build passes; new file lint-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- backup_crypto.py: Fernet file encrypt/decrypt using the existing
SERVERKIT_ENCRYPTION_KEY (no new deps) — encrypt_file (-> .enc),
decrypt_file, is_encrypted_backup, encryption_available.
- BackupService: an opt-in `encrypt_backups` config flag; _finalize_backup
encrypts each artifact before _auto_upload (so the stored/remote copy is
encrypted) and records encrypted/path/size; restore_application and
restore_database transparently decrypt a .enc backup to a temp file first.
- 9 tests: byte-exact roundtrip, wrong-key failure, availability reflects
env, is_encrypted_backup, and an encrypted backup_files create+restore.
Notes: in-memory (streaming large archives is a follow-up); key loss =
unrecoverable backups (back the key up out of band). Additional remote
targets (WebDAV/Azure/Dropbox/SFTP) and restore drills remain follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ContainerScalePolicy model (per-app: enabled, service_name, min/max
replicas, cpu high/low %, cooldown, current_replicas).
- ContainerScaleService: evaluate() scales a compose service up/down by one
replica when average CPU crosses the high/low thresholds, within bounds and
outside the cooldown; scale_to() for manual; sweep() for cron. Uses
`docker compose --scale`; local + scale-capable services only.
- apps.py: GET/PUT /<id>/scale-policy, POST /<id>/scale,
POST /<id>/scale/evaluate, POST /scale-sweep (admin).
- 10 tests: bounds clamping, scale up/down, hold-at-max, cooldown, disabled,
unknown-cpu, manual endpoint, auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GpuService shells out to nvidia-smi for per-GPU stats (utilization, VRAM +
memory %, temperature, power, fan, driver) and GPU compute processes with
best-effort PID->container resolution via cgroup. Coerces [N/A] to null and
degrades to "no GPUs" when nvidia-smi is absent.
- GET /api/v1/gpu/ returns {available, gpus, processes}.
- 6 tests: row parse, N/A coercion, availability, no-GPU info, endpoint, auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ContainerSleepPolicy model (per-app: enabled, idle_timeout_minutes,
last_activity_at, asleep) + a sleep badge in Application.to_dict.
- ContainerSleepService: sleep/wake an app (compose_down/up or
stop/start_container, local), record_activity, and sweep_idle() which
stops enabled+awake apps idle past their timeout — and never sleeps an app
with no activity baseline.
- apps.py endpoints: GET/PUT /<id>/sleep-policy, POST /<id>/sleep,
POST /<id>/wake, POST /sweep-idle (admin, cron-drivable).
- 9 tests: policy, sleep/wake, docker-failure, remote guard, sweep
selection, no-baseline skip, endpoints, auth.
Follow-ups: traffic-based idle detection (feed record_activity from the nginx
log) and request-triggered wake-on-demand; remote-server sleep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- POST /api/v1/apps/<id>/image-update/apply pulls the newest image for a
compose-managed app, then recreates it (compose_pull + compose_up),
reusing the existing local/remote deploy path (DockerService /
RemoteDockerService) exactly like start_app. Refreshes the image_update
badge afterward.
- Guarded to docker-compose apps; a standalone container would need its full
run spec to recreate, so those get a clear "update manually" message.
- 4 tests: pull-then-recreate ordering, pull-failure propagation, non-compose
guard, auth gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ImageUpdateCheck model + ImageUpdateService: compare an app's local
RepoDigest against the registry's current index digest (via
`docker buildx imagetools inspect`) for the same tag, and flag
update_available when they differ. Shells out to docker like the rest of
the Docker layer; degrades gracefully when the image is local-only or the
registry is unreachable.
- API: admin POST /api/v1/image-updates/applications/<id>/check and
GET /api/v1/image-updates/applications/<id> for the latest result.
- Application.to_dict gains a lightweight image_update badge (mirrors the
existing image_scan badge).
- 7 tests: digest diff/match/unknown, no-image, badge, endpoint, auth gate.
One-click apply (pull + recreate) to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DdnsHost model: a hostname bound to a DNS zone plus a secret token; the
token is masked in API responses except when first minted.
- DdnsService.update_ip reuses DNSZoneService so a configured provider
(e.g. Cloudflare) is synced automatically; chooses A vs AAAA by IP family;
no-op when the IP is unchanged.
- API: admin CRUD for hosts + a public, token-authenticated /ddns/update
endpoint that a router or cron calls when its IP changes (IP from ?ip= or
the request source). Wrong token -> 401, bad IP -> 400.
- 8 tests covering the service flow and both endpoints.
UI panel to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add 'topbar' to LayoutContext and a third switcher button (Sidebar |
Compact | Top bar).
- Topbar: the sidebar becomes a horizontal bar pinned to the top; top-level
items lay out in a horizontally-scrollable row with an underline active
indicator. Category headers, expand chevrons, and sub-items are hidden
(sub-nav is reached from each page's in-page navigation), which sidesteps
the horizontal-overflow vs. dropdown clipping trap. Desktop-only; the
mobile drawer is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New LayoutContext (localStorage-persisted, sets data-layout on <html>),
mirroring ThemeContext.
- "Layout" switcher in the sidebar user menu: Sidebar | Compact.
- Compact rail: a 64px icon-only sidebar that expands to the full sidebar
on hover/keyboard focus; desktop-only, the mobile drawer is unchanged.
- Reuses existing nav markup — labels hide via font-size:0 (kept in the
accessibility tree); no nav-render changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-generate SERVERKIT_ENCRYPTION_KEY in production when missing.
- Detect canonical domain from Host/X-Forwarded-Host; persist via SystemSettings.
- Expand CORS origins dynamically to include the canonical domain.
- Add SystemTab UI to view/save detected panel domain and HTTPS preference.
- Install script writes key, prompts for PANEL_DOMAIN, and sets nginx IP→domain redirect.
- Add backend tests for key bootstrap and domain detection helpers.
- Add webauthn dependency.
- Add PasskeyCredential model and Alembic migration 019_passkey_credentials.
- Add PasskeyService for registration/authentication challenge flow.
- Add auth API endpoints:
- POST /auth/passkeys/options/register
- POST /auth/passkeys/register
- GET /auth/passkeys
- DELETE /auth/passkeys/<id>
- POST /auth/passkeys/options/authenticate
- POST /auth/passkeys/authenticate
- Expose passkey_enabled flag in User.to_dict().
- Add ImageVulnerabilityScan and SbomArtifact models.
- Add ImageScannerService to download/install grype and syft into
/var/lib/serverkit/scanners, run CVE scans, generate SPDX SBOMs,
and enforce a configurable deploy gate.
- Add security API endpoints for scanner install, image scans,
scan history, deploy-gate checks, and SBOM create/download/list.
- Include lightweight image_scan badge in Application.to_dict() for UI.
- Add Alembic migration 018_image_scan_sbom.
- Add SERVERKIT_ENCRYPTION_KEY generation in install.sh.
- Ensure SERVERKIT_ENCRYPTION_KEY exists during update.sh runs.
- Encrypt/decrypt SSO client secrets, source GitHub client secret, and
AI API key in SettingsService transparently.
- Mask secret values in SettingsService.get_all() and
get_all_with_metadata() so the API never returns plaintext.
- Add SettingsService.migrate_legacy_secrets() and invoke it at startup
alongside DNS/storage/cloud legacy-secret migrations.
Simplify and clarify the MSI build comments in agent/packaging/msi/Product.wxs. Replace explicit dotnet tool/install commands with concise guidance to install WiX Toolset v5 and the matching Util extension (pinned to 5.0.2), and add a reference to agent/packaging/msi/build.ps1 for the exact CI commands. This makes the instructions shorter and points maintainers to the CI script for reproducible steps.
Use the correct WiX extension version specifier (wix extension add --global WixToolset.Util.wixext/5.0.2). Updated the GitHub workflow, MSI Product.wxs comments, and PowerShell build script message to use the slash-style version token so the extension installs correctly with wix v5.
Refactor agent GitHub Actions and build tooling to separate concerns: add a dedicated build-ui job (upload/download agent-ui-dist) and a version job to resolve the release version; make downstream jobs depend on these outputs. Update workflow steps to consume artifacts/outputs (use needs.version where appropriate), and mark multi-platform jobs to not rebuild the UI. Add Makefile UI targets (build-ui, UI_DIST dependency, cleanup) so go:embed succeeds locally. Improve Windows MSI docs and build script: require specific wix tool/version and verify WixToolset.Util extension before building.
Add a dedicated Node.js build stage to the agent Dockerfile to compile the desktop UI and embed its output into the Go binary. Copy the built UI into internal/agentui/dist before go build and make the Go build respect TARGETOS/TARGETARCH args for multi-arch builds. Also update agent/.dockerignore to exclude dist/, ui/dist/, and ui/node_modules/ to keep the image build context small and cache-friendly.
Switch MSI packaging to WiX Toolset v5: update CI to install the WiX Util extension, update Product.wxs comments and namespaces for v5 and show using the Util extension at build time, and enhance build.ps1 to pass the extension to wix build, fail loudly on non‑zero exit code, verify the MSI output exists and print the resolved output path. These changes ensure builds work with WiX v5 and provide clearer errors/output.