BazziteOS - Post Script

BazziteOS - Post Script
BassziteOS - Gaming on LInux

The folllowing script is a draft script and is provided as a guide.

IAW our ditching of the Microsoft eco system and the problems with MintOS in gaming we are moving various systems to BazziteOS. So far we have not been disappointed

The script is a draft. It is in testing, but offered as a guide

The following is noted:

  1. The printer section has been left in as a guide
  2. The UDM certificate section has been left in as a guide. This allows the UDM firewall to show the blocked page within systems that utilise a Ubiquiti network for end users. This is irrelevant for most networks.
  3. ClamAV code section has been pulled - I dont like it and in review.
  4. Script has been tested and modifications made.

ClamAV Problem

ClamAV was installed via distrobox and this was a mistake.

If you have used this script and run the ClamAV section the uninstall script is here:

The solution is ugly and uses to much space - so I have pulled that section and am reworking the code.

#!/bin/bash

# ─────────────────────────────────────────────────────────────────────────────
# undo-clamav-bazzite.sh
# Remove all ClamAV setup artefacts from Bazzite — all script versions
# ─────────────────────────────────────────────────────────────────────────────
#
# Removes artefacts left by setup-clamav.sh v4.0.0 and v4.1.0 on Bazzite:
#
#   Distrobox:
#     - clamav-box rootful container (if present)
#
#   System files (installed via sudo):
#     - /usr/local/bin/clamscan-host
#     - /etc/systemd/system/clamav-update.service
#     - /etc/systemd/system/clamav-update.timer
#
#   User-level systemd (v4.0.0 Homebrew artefact):
#     - ~/.config/systemd/user/clamav-freshclam.timer
#     - ~/.config/systemd/user/clamav-freshclam.service
#
#   rpm-ostree layer (if clamav was layered):
#     - clamav, clamav-update
#     NOTE: rpm-ostree uninstall requires a reboot to take effect.
#
# Run as: normal user (not root)
# Sudo:   called inline for system file removal and systemd units
#
# Usage:  ./undo-clamav-bazzite.sh [--dry-run]
# ─────────────────────────────────────────────────────────────────────────────

set -euo pipefail

info()    { echo -e "\033[0;32m[INFO]\033[0m  $*"; }
warn()    { echo -e "\033[0;33m[WARN]\033[0m  $*" >&2; }
error()   { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
section() { echo -e "\n\033[1;34m>>> $* \033[0m"; }

DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
    DRY_RUN=true
    info "Dry-run mode — no changes will be made."
fi

if [[ "${EUID}" -eq 0 ]]; then
    error "Run as your normal user, not root."
    exit 1
fi

REAL_USER="$(id -un)"
info "Running as: $REAL_USER"

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1 — Stop and remove clamav-update systemd timer
# ─────────────────────────────────────────────────────────────────────────────
section "Removing clamav-update systemd timer"

for unit in clamav-update.timer clamav-update.service; do
    if sudo systemctl is-active --quiet "$unit" 2>/dev/null; then
        if $DRY_RUN; then
            info "[DRY] Would stop: $unit"
        else
            sudo systemctl stop "$unit" && info "Stopped: $unit"
        fi
    fi
    if sudo systemctl is-enabled --quiet "$unit" 2>/dev/null; then
        if $DRY_RUN; then
            info "[DRY] Would disable: $unit"
        else
            sudo systemctl disable "$unit" && info "Disabled: $unit"
        fi
    fi
done

for unit_file in \
    /etc/systemd/system/clamav-update.service \
    /etc/systemd/system/clamav-update.timer; do
    if [[ -f "$unit_file" ]]; then
        if $DRY_RUN; then
            info "[DRY] Would remove: $unit_file"
        else
            sudo rm -f "$unit_file" && info "Removed: $unit_file"
        fi
    else
        info "Not present: $unit_file"
    fi
done

if $DRY_RUN; then
    info "[DRY] Would run: sudo systemctl daemon-reload"
else
    sudo systemctl daemon-reload
    info "systemctl daemon-reload complete."
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2 — Remove clamscan-host wrapper
# ─────────────────────────────────────────────────────────────────────────────
section "Removing clamscan-host wrapper"

WRAPPER="/usr/local/bin/clamscan-host"
if [[ -f "$WRAPPER" ]]; then
    if $DRY_RUN; then
        info "[DRY] Would remove: $WRAPPER"
    else
        sudo rm -f "$WRAPPER" && info "Removed: $WRAPPER"
    fi
else
    info "Not present: $WRAPPER"
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 — Remove distrobox clamav-box container
# ─────────────────────────────────────────────────────────────────────────────
section "Removing clamav-box Distrobox container"

if ! command -v distrobox &>/dev/null; then
    info "Distrobox not installed — skipping container removal."
else
    if distrobox list --root 2>/dev/null | grep -q "clamav-box"; then
        if $DRY_RUN; then
            info "[DRY] Would run: distrobox rm --root --force clamav-box"
        else
            info "Removing clamav-box container..."
            distrobox rm --root --force clamav-box \
                && info "clamav-box removed." \
                || warn "clamav-box removal failed — try manually: distrobox rm --root --force clamav-box"
        fi
    else
        info "clamav-box container not present — nothing to remove."
    fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 4 — Remove user-level systemd timer (v4.0.0 Homebrew artefact)
# ─────────────────────────────────────────────────────────────────────────────
section "Removing legacy user-level systemd timer (v4.0.0)"

USER_SYSTEMD_DIR="${HOME}/.config/systemd/user"

for unit in clamav-freshclam.timer clamav-freshclam.service; do
    if systemctl --user is-active --quiet "$unit" 2>/dev/null; then
        if $DRY_RUN; then
            info "[DRY] Would stop user unit: $unit"
        else
            systemctl --user stop "$unit" 2>/dev/null || true
            info "Stopped user unit: $unit"
        fi
    fi
    if systemctl --user is-enabled --quiet "$unit" 2>/dev/null; then
        if $DRY_RUN; then
            info "[DRY] Would disable user unit: $unit"
        else
            systemctl --user disable "$unit" 2>/dev/null || true
            info "Disabled user unit: $unit"
        fi
    fi
done

for unit_file in \
    "${USER_SYSTEMD_DIR}/clamav-freshclam.timer" \
    "${USER_SYSTEMD_DIR}/clamav-freshclam.service"; do
    if [[ -f "$unit_file" ]]; then
        if $DRY_RUN; then
            info "[DRY] Would remove: $unit_file"
        else
            rm -f "$unit_file" && info "Removed: $unit_file"
        fi
    else
        info "Not present: ${unit_file##*/}"
    fi
done

if [[ -d "$USER_SYSTEMD_DIR" ]]; then
    if $DRY_RUN; then
        info "[DRY] Would run: systemctl --user daemon-reload"
    else
        systemctl --user daemon-reload 2>/dev/null || true
        info "User systemd reloaded."
    fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 5 — Remove rpm-ostree layered clamav packages (if present)
# ─────────────────────────────────────────────────────────────────────────────
section "Checking rpm-ostree layered ClamAV packages"

LAYERED=$(rpm-ostree status --json 2>/dev/null \
    | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    pkgs = d['deployments'][0].get('requested-packages', [])
    clamav = [p for p in pkgs if 'clamav' in p]
    print(' '.join(clamav))
except:
    pass
" 2>/dev/null || true)

if [[ -n "$LAYERED" ]]; then
    warn "Found rpm-ostree layered ClamAV packages: $LAYERED"
    if $DRY_RUN; then
        info "[DRY] Would run: sudo rpm-ostree uninstall $LAYERED"
        info "[DRY] Would require a reboot to take effect"
    else
        info "Removing layered packages: $LAYERED"
        sudo rpm-ostree uninstall $LAYERED \
            && warn "rpm-ostree uninstall queued — REBOOT REQUIRED to complete removal." \
            || warn "rpm-ostree uninstall failed — check: sudo rpm-ostree status"
    fi
else
    info "No ClamAV packages found in rpm-ostree layered packages."
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 — Remove Homebrew clamav (v4.0.0 artefact, if present)
# ─────────────────────────────────────────────────────────────────────────────
section "Checking Homebrew ClamAV (v4.0.0 artefact)"

if command -v brew &>/dev/null; then
    if brew list clamav &>/dev/null 2>&1; then
        if $DRY_RUN; then
            info "[DRY] Would run: brew uninstall clamav"
        else
            brew uninstall clamav \
                && info "Homebrew clamav removed." \
                || warn "brew uninstall failed — try manually: brew uninstall clamav"
        fi
    else
        info "Homebrew clamav not installed — nothing to remove."
    fi
else
    info "Homebrew not present — nothing to remove."
fi

# ─────────────────────────────────────────────────────────────────────────────
# COMPLETE
# ─────────────────────────────────────────────────────────────────────────────
section "Undo Complete"

echo ""
echo "  ┌─────────────────────────────────────────────────────────────┐"
echo "  │  ClamAV Bazzite Undo Complete                               │"
echo "  ├─────────────────────────────────────────────────────────────┤"
echo "  │  Removed (if present):                                      │"
echo "  │    clamav-update.timer / .service  (system)                 │"
echo "  │    /usr/local/bin/clamscan-host                             │"
echo "  │    clamav-box Distrobox container                           │"
echo "  │    User-level clamav-freshclam timer (v4.0.0)               │"
echo "  │    Homebrew clamav (v4.0.0)                                 │"
echo "  ├─────────────────────────────────────────────────────────────┤"
if [[ -n "${LAYERED:-}" ]]; then
echo "  │  *** REBOOT REQUIRED ***                                    │"
echo "  │  rpm-ostree uninstall queued for: ${LAYERED}                │"
fi
echo "  │  System is clean — ready for fresh ClamAV setup.           │"
echo "  └─────────────────────────────────────────────────────────────┘"
echo ""

Post BazziteOS setup script.

#!/bin/bash

# ================================================================================
# Base Setup Script for Bazzite OS (Fedora Atomic / rpm-ostree)
# Filename : setup-laptop-bazzite.sh
# Updated  : 2026-05-17
# Version  : 1.1.0
# ================================================================================
#
# PURPOSE:
#   Post-install hardening and configuration for a Bazzite desktop/laptop.
#   Bazzite is an IMMUTABLE OS — the root filesystem is read-only and managed
#   via rpm-ostree atomic image updates. This script respects that model:
#
#     • System packages  → rpm-ostree install  (requires reboot to activate)
#     • GUI applications → Flatpak (Flathub)
#     • CLI/TUI tools    → Homebrew (brew) where possible
#     • Anything else    → Distrobox containers
#
#   DO NOT use apt-get, dpkg, dnf, or yum directly on the host.
#   DO NOT attempt to install .deb packages — this is Fedora/RPM based.
#
# WHAT THIS SCRIPT DOES:
#
#   SECURITY & HARDENING:
#     - Hardens SSH via sshd_config.d drop-in (survives image updates)
#     - Regenerates SSH host keys (ED25519 + RSA 4096)
#     - ClamAV antivirus installed via rpm-ostree, run as a rootful Distrobox
#       container to avoid conflicts with the immutable OS image
#     - Configures firewalld (Bazzite's native firewall) with default-deny
#       incoming policy across all zones, preserving SSH and WireGuard egress
#     - UDM SE block-page certificate installed to system trust store
#
#   SOFTWARE INSTALLATION (Flatpak via Flathub):
#     - GIMP, Inkscape, Krita, LibreOffice, VLC, Pinta
#     - Google Chrome (Flatpak — avoids APT/rpm-ostree repo complexity)
#
#   SYSTEM TOOLS (rpm-ostree layered — requires reboot):
#     - wireguard-tools (kernel module already included in Bazzite)
#     - curl, wget, nmap, smartmontools, mtr, rsyslog, fwupd
#     - openssh-server (usually pre-installed; layered defensively)
#
#   INSYNC (Google Drive client):
#     - Downloads Insync Fedora 44 RPM directly from cdn.insynchq.com
#       (bypasses the yum repo — avoids GPG DB write on immutable root and
#       the fc44 repo 404 that affects $releasever-based repo configs)
#     - Falls back to the working fc43 repo URL if the CDN download fails
#     - Detects KDE vs GNOME at runtime for file manager integration
#     - Layers insync via rpm-ostree (requires reboot)
#
#   SYSTEM CONFIGURATION:
#     - Epson WF-4830 printer via IPP Everywhere
#     - IPv6 stable address preference (disables temp addresses on wlo1)
#     - Automatic OS and Flatpak updates via ujust / rpm-ostree
#     - Custom MOTD (OS, IP, uptime, IPv4/IPv6)
#
#   FIRMWARE UPDATES (fwupd):
#     - LVFS metadata refresh
#     - AC power and battery pre-flight checks
#     - BIOS/UEFI capsule gating on AC power
#     - Non-BIOS firmware applied unconditionally
#
#   DIAGNOSTICS:
#     - Final health check covering firewall, SSH, ClamAV, Insync,
#       systemd units, and rpm-ostree deployment status
#
# WHAT THIS SCRIPT DOES NOT DO:
#   - AppArmor: not applicable — Bazzite uses SELinux (Fedora default)
#   - UFW: not applicable — Bazzite uses firewalld
#   - apt/dpkg/APT repos: not applicable — Fedora RPM ecosystem only
#   - i386 multiarch: not applicable — handled by Flatpak/Steam runtime
#   - Novabench: no safe RPM/Flatpak available; use Phoronix Test Suite
#     inside a Distrobox container if benchmarking is required
#
# ADDITIONAL NOTES:
#   a) AUTOMATIC UPDATES: Bazzite updates are atomic via rpm-ostree. This
#      script enables the rpm-ostreed-automatic timer and configures Flatpak
#      auto-updates. OS updates are staged and applied on next reboot.
#
#   b) WAYDROID (Android / Play Store): Bazzite has native Waydroid support
#      via `ujust setup-waydroid`. This is a graphical, interactive setup and
#      CANNOT be fully automated from a non-interactive script. Instructions
#      are printed at the end of this script. Run them manually after reboot.
#
#   c) SECURITY NOTE: All Debian/LMDE-specific constructs (apt hold, dpkg,
#      /etc/apt/*, AppArmor, update-ca-certificates) have been replaced with
#      their Fedora/Bazzite equivalents. No Debian-origin commands remain.
#
# REBOOT REQUIRED:
#   rpm-ostree layers packages into a new pending deployment. A reboot is
#   REQUIRED at the end of this script for all layered packages to activate.
#   The script will prompt before rebooting, or you can reboot manually.
#
# ================================================================================
#
# CHANGELOG:
#   1.1.0 - 2026-05-17 - Three fixes confirmed via live test run:
#                         (1) Insync: replaced rpm --import + yum repo approach
#                             with direct CDN RPM download (cdn.insynchq.com).
#                             rpm --import fails on Bazzite — RPM DB is on the
#                             immutable image layer. CDN download also bypasses
#                             the fc44 yum repo 404 ($releasever resolving to
#                             44 when Insync's repo only published up to fc43).
#                             Fallback to fc43 repo URL added if CDN is down.
#                         (2) Firewalld: removed --permanent from
#                             --set-default-zone (firewalld rejects that combo;
#                             the default zone change is always permanent).
#                             Added DHCPv6-client rule omitted in v1.0.0.
#                         (3) DE detection improved: now checks running desktop
#                             processes (plasmashell, gnome-shell) as fallback
#                             when XDG_CURRENT_DESKTOP is unset under sudo.
#   1.0.0 - 2026-05-15 - Initial Bazzite port from LMDE 7 base-setup v2.3.0.
#                         Complete rewrite for rpm-ostree/Fedora Atomic model.
#                         Removed: apt/dpkg, AppArmor, UFW, i386, Novabench,
#                         user-specific braedach block, Chrome APT repo.
#                         Added: firewalld hardening, rpm-ostree layering,
#                         Insync RPM repo, ClamAV Distrobox strategy,
#                         automatic update configuration, final diagnostics.
#
# ================================================================================

set -euo pipefail

# -------------------------------------------------------------------------------
# Privilege check

if [[ "${EUID}" -ne 0 ]]; then
    echo "[ERROR] This script must be run as root (sudo ./setup-laptop-bazzite.sh)"
    exit 1
fi

# -------------------------------------------------------------------------------
# Logging

LOGFILE="/var/log/bazzite-setup.log"
BACKUP_TS="$(date +%Y%m%d-%H%M%S)"

exec > >(tee -a "$LOGFILE") 2>&1

info()  { echo -e "\033[0;32m[INFO]\033[0m  $*"; }
warn()  { echo -e "\033[0;33m[WARN]\033[0m  $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
step()  { echo -e "\n\033[1;34m[====]\033[0m $*\n"; }

info "Bazzite setup started at $(date)"
info "Log file: ${LOGFILE}"

# -------------------------------------------------------------------------------
# Resolve logged-in user (not root, even when run via sudo)

CURRENT_USER=$(logname 2>/dev/null || echo "${SUDO_USER:-root}")
CURRENT_HOME=$(getent passwd "${CURRENT_USER}" | cut -d: -f6)
info "Running as root. Detected user context: ${CURRENT_USER} (home: ${CURRENT_HOME})"

# -------------------------------------------------------------------------------
# Sanity check: confirm we are on a Fedora/Bazzite system

if ! command -v rpm-ostree &>/dev/null; then
    error "rpm-ostree not found. This script is for Bazzite / Fedora Atomic only."
fi

OS_ID=$(grep -oP '(?<=^ID=).*' /etc/os-release | tr -d '"' || echo "unknown")
OS_PRETTY=$(grep -oP '(?<=^PRETTY_NAME=).*' /etc/os-release | tr -d '"' || echo "unknown")
info "Detected OS: ${OS_PRETTY}"

# -------------------------------------------------------------------------------
# Network connectivity check

step "Network Connectivity"
info "Checking network connectivity..."
if ! curl -fsS --max-time 10 https://flathub.org > /dev/null 2>&1; then
    error "Network check failed — cannot reach flathub.org. Check DNS/network before continuing."
fi
info "Network OK."

# -------------------------------------------------------------------------------
# Reset any failed systemd units from a prior run

step "Systemd Housekeeping"
info "Checking and resetting failed systemd units..."
systemctl reset-failed || true

# ===============================================================================
# SECTION 1: rpm-ostree package layering
#
# All packages listed here are added to the pending deployment.
# They will only be ACTIVE after the reboot at the end of this script.
# Keep this list minimal — prefer Flatpak, Homebrew, or Distrobox instead.
# ===============================================================================

step "rpm-ostree Package Layering"

info "Layering system packages via rpm-ostree..."
info "NOTE: These packages activate after the post-script reboot."

# wireguard-tools: kernel WireGuard module is already in Bazzite's kernel.
# This adds the wg / wg-quick CLI tools for managing configs.
RPM_PACKAGES=(
    curl
    wget
    nmap
    mtr
    smartmontools
    rsyslog
    fwupd
    openssh-server
    wireguard-tools
    cups                  # required for printer setup
    avahi                 # required for IPP Everywhere / mDNS printer discovery
)

# Build a space-separated list
PKG_LIST="${RPM_PACKAGES[*]}"

if rpm-ostree install --idempotent --allow-inactive ${PKG_LIST}; then
    info "rpm-ostree layering queued successfully."
    info "Packages will be active after reboot."
else
    warn "rpm-ostree install reported an issue — check output above."
    warn "Some packages may already be installed (--idempotent handles this)."
fi

# ===============================================================================
# SECTION 2: Insync (Google Drive client)
#
# Insync is a commercial GUI Google Drive/OneDrive sync client for Linux.
# ($29.99/account, 15-day free trial)
#
# WHY DIRECT CDN DOWNLOAD (not the yum repo):
#   Two confirmed failures with the repo approach on Bazzite/Fedora 44:
#   1. rpm --import cannot write to /usr/share/rpm/.rpm.lock — that path is
#      on the immutable image layer and is read-only on Bazzite.
#   2. https://yum.insynchq.com/fedora/44/ returns 404. Insync's repo lags
#      behind Fedora releases; the $releasever variable resolves to 44 but
#      Insync's infrastructure only had fc43 at Fedora 44 GA.
#
#   Solution: download the native fc44 RPM directly from cdn.insynchq.com
#   and install it as a local file via rpm-ostree. No repo or GPG DB write
#   required. A fallback to the working fc43 repo URL is attempted if the
#   CDN download fails.
# ===============================================================================

step "Insync (Google Drive Client)"

INSYNC_CDN_BASE="https://cdn.insynchq.com/builds/linux"
INSYNC_RPM_TMP="/tmp/insync-fc44.x86_64.rpm"
INSYNC_KDE_RPM_TMP="/tmp/insync-kde-fc44.rpm"
INSYNC_RPM_URL="${INSYNC_CDN_BASE}/insync-3.9.8.60034-fc44.x86_64.rpm"

# Detect desktop environment — XDG_CURRENT_DESKTOP is usually unset when
# running as root, so fall back to checking running desktop processes.
ACTUAL_DE="${XDG_CURRENT_DESKTOP:-}"
if [[ -z "${ACTUAL_DE}" ]] && [[ -n "${SUDO_USER:-}" ]]; then
    ACTUAL_DE=$(sudo -u "${SUDO_USER}" bash -c 'echo "${XDG_CURRENT_DESKTOP:-}"' 2>/dev/null || true)
fi
if [[ -z "${ACTUAL_DE}" ]]; then
    if pgrep -x plasmashell &>/dev/null || pgrep -x kwin_wayland &>/dev/null; then
        ACTUAL_DE="KDE"
    elif pgrep -x gnome-shell &>/dev/null; then
        ACTUAL_DE="GNOME"
    fi
fi
info "Detected desktop environment: ${ACTUAL_DE:-unknown}"

info "Downloading Insync Fedora 44 RPM from CDN..."
info "URL: ${INSYNC_RPM_URL}"

if curl -fsSL --retry 3 --retry-delay 5 -o "${INSYNC_RPM_TMP}" "${INSYNC_RPM_URL}"; then
    info "Insync RPM downloaded successfully."
    if rpm-ostree install --idempotent --allow-inactive "${INSYNC_RPM_TMP}"; then
        info "Insync layered successfully — will be active after reboot."
    else
        warn "rpm-ostree install of Insync RPM failed."
        warn "Install manually after reboot:"
        warn "  curl -fsSL ${INSYNC_RPM_URL} -o /tmp/insync.rpm"
        warn "  sudo rpm-ostree install /tmp/insync.rpm"
    fi
    rm -f "${INSYNC_RPM_TMP}"
else
    warn "CDN download failed — attempting fallback via fc43 repo URL..."
    cat > /etc/yum.repos.d/insync.repo <<'EOF'
[insync]
name=Insync repo (Fedora 43 — fc44 compatible)
baseurl=http://yum.insync.io/fedora/43/
gpgcheck=0
enabled=1
metadata_expire=120m
EOF
    info "Fallback repo written (/etc/yum.repos.d/insync.repo)."
    if rpm-ostree install --idempotent --allow-inactive insync; then
        info "Insync installed via fallback fc43 repo."
    else
        warn "Insync install failed via fallback repo too."
        warn "Download manually from: https://www.insynchq.com/downloads/linux#fedora"
        warn "Then: sudo rpm-ostree install /path/to/insync-*.fc44.x86_64.rpm"
    fi
fi

# File manager integration (KDE only — GNOME integration is bundled in main package)
if echo "${ACTUAL_DE}" | grep -qi "kde\|plasma"; then
    info "KDE detected — installing insync-dolphin integration..."
    INSYNC_KDE_URL="${INSYNC_CDN_BASE}/insync-dolphin-3.9.8.60034-fc44.x86_64.rpm"
    if curl -fsSL --retry 2 -o "${INSYNC_KDE_RPM_TMP}" "${INSYNC_KDE_URL}" 2>/dev/null; then
        rpm-ostree install --idempotent --allow-inactive "${INSYNC_KDE_RPM_TMP}" \
            && info "insync-dolphin layered." \
            || warn "insync-dolphin layer failed — install manually after reboot."
        rm -f "${INSYNC_KDE_RPM_TMP}"
    else
        warn "insync-dolphin RPM not found at CDN — install manually after reboot:"
        warn "  sudo rpm-ostree install insync-dolphin"
    fi
else
    info "GNOME or unknown DE — Nautilus integration is bundled in the main Insync package."
fi

# ===============================================================================
# SECTION 3: Flatpak Applications
#
# Flatpak is the primary app delivery mechanism on Bazzite.
# These apps run sandboxed and update independently of the OS image.
# ===============================================================================

step "Flatpak Application Installation"

# Ensure Flathub remote is present (it should be on Bazzite, but be safe)
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || true

FLATPAK_APPS=(
    "org.gimp.GIMP"
    "org.inkscape.Inkscape"
    "org.kde.krita"
    "org.libreoffice.LibreOffice"
    "org.videolan.VLC"
    "com.github.PintaProject.Pinta"
    "com.google.Chrome"
)

for app in "${FLATPAK_APPS[@]}"; do
    info "Installing Flatpak: ${app}..."
    if flatpak install -y --noninteractive flathub "${app}" 2>/dev/null; then
        info "  ✓ ${app}"
    else
        warn "  ✗ ${app} — install failed or already installed."
    fi
done

# ===============================================================================
# SECTION 4: Firewalld Configuration
#
# Bazzite uses firewalld (not UFW or nftables directly).
# firewalld sits on top of nftables on modern Fedora.
#
# Policy:
#   - Default incoming: DROP (block all unsolicited inbound)
#   - Default outgoing: ACCEPT (all egress permitted)
#   - SSH allowed from LAN (192.168.0.0/16) only
#   - WireGuard egress UDP 51820 allowed outbound (inherent in default-allow out)
#   - WireGuard inbound port opened only in 'home' zone if needed
#   - All changes saved to permanent config (survive reboot)
# ===============================================================================

step "Firewalld Configuration"

if ! command -v firewall-cmd &>/dev/null; then
    warn "firewall-cmd not found — skipping firewall configuration."
    warn "Install firewalld and rerun this section manually."
else

    info "Ensuring firewalld is enabled and running..."
    systemctl enable --now firewalld || warn "firewalld enable/start failed."

    # ---- Set default zone to 'drop' (silently discard all inbound) ----
    # NOTE: --set-default-zone is ALWAYS permanent (written to firewalld.conf).
    # firewalld rejects --permanent combined with --set-default-zone — do not add it.
    info "Setting default zone to 'drop' (default-deny all inbound)..."
    firewall-cmd --set-default-zone=drop

    # ---- SSH access from LAN (RFC1918 IPv4) ----
    info "Adding SSH rich rule: permit from RFC1918 only..."
    firewall-cmd --permanent --zone=drop \
        --add-rich-rule='rule family="ipv4" source address="192.168.0.0/16" service name="ssh" accept' \
        2>/dev/null || info "SSH IPv4 rule already exists."

    # ---- IPv6 ULA and link-local SSH access ----
    firewall-cmd --permanent --zone=drop \
        --add-rich-rule='rule family="ipv6" source address="fe80::/10" service name="ssh" accept' \
        2>/dev/null || info "IPv6 link-local SSH rule already exists."

    firewall-cmd --permanent --zone=drop \
        --add-rich-rule='rule family="ipv6" source address="fc00::/7" service name="ssh" accept' \
        2>/dev/null || info "IPv6 ULA SSH rule already exists."

    # ---- mDNS for printer discovery (LAN only, not internet-facing) ----
    firewall-cmd --permanent --zone=drop \
        --add-rich-rule='rule family="ipv4" source address="192.168.0.0/16" service name="mdns" accept' \
        2>/dev/null || info "mDNS rule already exists."

    # ---- DHCPv6 client (required for IPv6 address assignment on most networks) ----
    firewall-cmd --permanent --zone=drop \
        --add-service=dhcpv6-client \
        2>/dev/null || info "DHCPv6-client rule already exists."

    # ---- WireGuard inbound port ----
    # This machine is a WireGuard CLIENT only — egress to UDP 51820 is covered
    # by the default-allow-out policy. Uncomment only if this machine needs to
    # ACCEPT inbound WireGuard connections (i.e. acts as a server/endpoint):
    # firewall-cmd --permanent --zone=drop --add-port=51820/udp

    # ---- Reload to apply permanent changes ----
    info "Reloading firewalld to apply configuration..."
    firewall-cmd --reload

    info "Firewalld configuration complete."
    info "Active configuration:"
    firewall-cmd --list-all --zone=drop || true

fi

# ===============================================================================
# SECTION 5: SSH Hardening
#
# OpenSSH server is pre-installed on Bazzite. We harden it using a drop-in
# config in /etc/ssh/sshd_config.d/ which survives image updates.
#
# Key changes from the LMDE version:
#  - Service name is 'sshd' (not 'ssh') on Fedora/Bazzite
#  - sftp subsystem path is /usr/libexec/openssh/sftp-server on Fedora
#  - sshd_config is not writable on the immutable root — we use sshd_config.d
# ===============================================================================

step "SSH Hardening"

SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
SSHD_CUSTOM_DIR="${SSHD_DIR}/sshd_config.d"
SSHD_CUSTOM_CFG="${SSHD_CUSTOM_DIR}/99-hardened-custom.conf"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"

mkdir -p "${BACKUP_DIR}"
mkdir -p "${SSHD_CUSTOM_DIR}"

info "Backing up existing sshd_config and host keys to ${BACKUP_DIR}..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" 2>/dev/null || \
    warn "sshd_config not yet present (pending rpm-ostree reboot) — will configure keys post-reboot."

for key in ssh_host_ed25519_key ssh_host_rsa_key ssh_host_ecdsa_key; do
    [[ -f "${SSHD_DIR}/${key}"     ]] && cp -a "${SSHD_DIR}/${key}"     "${BACKUP_DIR}/" || true
    [[ -f "${SSHD_DIR}/${key}.pub" ]] && cp -a "${SSHD_DIR}/${key}.pub" "${BACKUP_DIR}/" || true
done

# Regenerate host keys only if the openssh-server keys exist
# (they may not exist until after the rpm-ostree reboot if openssh-server was just layered)
if [[ -d /etc/ssh ]]; then

    info "Removing old SSH host keys..."
    rm -f /etc/ssh/ssh_host_*

    info "Generating new ED25519 host key..."
    ssh-keygen -t ed25519 -f "${SSHD_DIR}/ssh_host_ed25519_key" -N "" -o -a 100

    info "Generating new RSA 4096 host key..."
    ssh-keygen -t rsa -b 4096 -f "${SSHD_DIR}/ssh_host_rsa_key" -N "" -o

    chmod 600 "${SSHD_DIR}/ssh_host_ed25519_key" "${SSHD_DIR}/ssh_host_rsa_key"
    chmod 644 "${SSHD_DIR}/ssh_host_ed25519_key.pub" "${SSHD_DIR}/ssh_host_rsa_key.pub"

fi

# Ensure main sshd_config includes the drop-in directory
if [[ -f "${SSHD_CFG}" ]] && ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
    # On Fedora Atomic the base config file is on the immutable layer.
    # The Include directive may already exist. Check before patching.
    if grep -q "^Include" "${SSHD_CFG}"; then
        info "Include directive already present in sshd_config."
    else
        # /etc/ is writable (it's a separate overlay), so this is safe.
        sed -i "1i Include ${SSHD_CUSTOM_DIR}/*.conf" "${SSHD_CFG}"
        info "Added Include directive to ${SSHD_CFG}."
    fi
fi

info "Writing hardened SSH drop-in to ${SSHD_CUSTOM_CFG}..."

cat > "${SSHD_CUSTOM_CFG}" <<EOF
# Hardened SSH Configuration — Bazzite
# Managed by setup-laptop-bazzite.sh (${BACKUP_TS})
# Drop-in file: survives rpm-ostree image updates.

Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
HostbasedAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10

HostKey ${SSHD_DIR}/ssh_host_ed25519_key
HostKey ${SSHD_DIR}/ssh_host_rsa_key

AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
PrintMotd no
PrintLastLog yes
AcceptEnv LANG LC_*

# Subsystem path on Fedora/Bazzite (differs from Debian)
Subsystem sftp /usr/libexec/openssh/sftp-server

# Permit password auth from LAN (RFC1918), ULA, and link-local IPv6 only
Match Address 192.168.0.0/16,fe80::/10,fc00::/7
    PermitRootLogin no
    PasswordAuthentication yes
    AllowTcpForwarding no

EOF

chmod 644 "${SSHD_CUSTOM_CFG}"

# Enable and start sshd
info "Enabling and starting sshd service..."
systemctl enable sshd || warn "sshd enable failed (may not be active until reboot)."
systemctl start sshd 2>/dev/null || warn "sshd start failed — openssh-server may not be active until after reboot."

# Validate config if sshd binary is present
if command -v sshd &>/dev/null; then
    if sshd -t 2>/dev/null; then
        info "sshd_config syntax OK."
        systemctl restart sshd 2>/dev/null || warn "sshd restart failed."
    else
        warn "sshd_config test failed — check ${SSHD_CUSTOM_CFG}"
        warn "Backup at: ${BACKUP_DIR}/sshd_config.bak"
    fi
else
    info "sshd binary not yet active (will be after reboot). Config written."
fi

# ===============================================================================
# SECTION 6: ClamAV
#
# The Bazzite immutable image ships without ClamAV. Layering it via rpm-ostree
# Pull this part of the code - its wrong and doesnt meet requirements
# ===============================================================================

step "ClamAV (via Distrobox Container - nope not pretty)"

warn "Code has been pulled - extensive tesing in progress ..."

# ===============================================================================
# SECTION 7: UDM SE Certificate
#
# Install the Ubiquiti UDM SE block-page CA certificate into the system
# trust store. On Fedora/Bazzite the tool is 'update-ca-trust' (not
# 'update-ca-certificates' which is Debian-specific).
#
# Certificate store location: /etc/pki/ca-trust/source/anchors/
# ===============================================================================

step "UDM SE Block-Page Certificate"

CERT_URL="http://ghost.baden.braedach.com/content/files/2025/12/UniFi-SSL-Certificate.cer"
CERT_TMP="/tmp/UniFi-SSL-Certificate.cer"
CERT_DST="/etc/pki/ca-trust/source/anchors/udmse-blockpage-ca.crt"

info "Downloading UDM SE certificate from ${CERT_URL}..."

if ! wget -q -O "${CERT_TMP}" "${CERT_URL}"; then
    warn "Failed to download certificate from ${CERT_URL}"
    warn "Ensure ghost.baden.braedach.com is reachable from this machine."
else
    if ! grep -q "BEGIN CERTIFICATE" "${CERT_TMP}"; then
        warn "Downloaded file is not a valid PEM certificate — skipping install."
        rm -f "${CERT_TMP}"
    else
        cp "${CERT_TMP}" "${CERT_DST}"
        if update-ca-trust extract; then
            info "UDM SE block-page certificate installed successfully."
            info "Installed to: ${CERT_DST}"
        else
            warn "update-ca-trust failed — check if the file is valid PEM."
        fi
        rm -f "${CERT_TMP}"
    fi
fi

# ===============================================================================
# SECTION 8: IPv6 Stable Address Configuration
#
# Disables IPv6 temporary/privacy addresses on wlo1 to prevent mDNS/Avahi
# resolution failures caused by address churn. Uses sysctl drop-in.
# /etc/sysctl.d/ is writable on Bazzite (/etc is an overlay).
# ===============================================================================

step "IPv6 Configuration"

info "Configuring IPv6 stable addressing..."

cat > /etc/sysctl.d/99-ipv6-laptop.conf <<'EOF'
# Prefer stable IPv6 addresses over temporary privacy addresses.
# Reduces mDNS/Avahi resolution failures caused by address churn on wlo1.
net.ipv6.conf.wlo1.use_tempaddr=0
net.ipv6.conf.all.use_tempaddr=0
net.ipv6.conf.default.use_tempaddr=0
EOF

sysctl -p /etc/sysctl.d/99-ipv6-laptop.conf || warn "IPv6 sysctl apply failed."

# ===============================================================================
# SECTION 9: Printer Setup (Epson WF-4830)
#
# CUPS / avahi have been queued in the rpm-ostree layer above.
# lpadmin will fail if CUPS is not yet active (needs the rpm-ostree reboot).
# We attempt it here; if it fails, a post-reboot reminder is printed.
# ===============================================================================

step "Epson WF-4830 Printer"

info "Attempting to install Epson WF-4830 via IPP Everywhere..."

if systemctl is-active --quiet cups 2>/dev/null; then
    if lpadmin -p "EPSON-WF4830" \
        -E \
        -v "ipp://192.168.1.11/ipp/print" \
        -m everywhere \
        -D "EPSON WF-4830 Series"; then
        info "Printer installed successfully."
        lpoptions -d EPSON-WF4830 || true
    else
        warn "lpadmin failed. Try again after reboot once CUPS is active."
    fi
else
    warn "CUPS service not running yet (expected before rpm-ostree reboot)."
    warn "Run after reboot: sudo lpadmin -p EPSON-WF4830 -E -v ipp://192.168.1.11/ipp/print -m everywhere -D 'EPSON WF-4830 Series'"
fi

# ===============================================================================
# SECTION 10: Firmware Updates (fwupd)
# ===============================================================================

step "Firmware Updates (fwupd)"

if ! command -v fwupdmgr &>/dev/null; then
    warn "fwupdmgr not found — queued via rpm-ostree, will be available after reboot."
else

    # AC power pre-flight
    AC_ONLINE=0
    for ps in /sys/class/power_supply/AC*/online /sys/class/power_supply/ADP*/online; do
        [[ -f "${ps}" ]] && AC_ONLINE=$(cat "${ps}") && break
    done

    if [[ "${AC_ONLINE}" -eq 1 ]]; then
        info "AC power confirmed — BIOS/UEFI capsule updates eligible."
    else
        warn "AC power NOT detected. BIOS/UEFI updates will be blocked by fwupd."
        warn "Connect AC adapter before running if a BIOS update is needed."
    fi

    # Battery pre-flight
    BAT_CAPACITY=""
    for bat in /sys/class/power_supply/BAT*/capacity; do
        [[ -f "${bat}" ]] && BAT_CAPACITY=$(cat "${bat}") && break
    done

    if [[ -n "${BAT_CAPACITY}" ]]; then
        if [[ "${BAT_CAPACITY}" -ge 10 ]]; then
            info "Battery level: ${BAT_CAPACITY}% — meets fwupd minimum."
        else
            warn "Battery: ${BAT_CAPACITY}% — below fwupd minimum of 10%."
        fi
    else
        info "No battery detected — skipping battery check."
    fi

    # Refresh LVFS metadata
    info "Refreshing LVFS firmware metadata..."
    if ! fwupdmgr refresh --force; then
        warn "LVFS metadata refresh failed — check connectivity."
    else
        info "Querying devices visible to fwupd..."
        fwupdmgr get-devices || warn "Could not enumerate fwupd devices."

        info "Checking for available firmware updates..."
        UPDATE_OUTPUT=$(fwupdmgr get-updates 2>&1 || true)

        if echo "${UPDATE_OUTPUT}" | grep -qi "no upgrades\|nothing to do\|no.*update"; then
            info "System firmware is already up to date."
        else
            info "Firmware updates available:"
            echo "${UPDATE_OUTPUT}"

            if echo "${UPDATE_OUTPUT}" | grep -qi "system firmware\|uefi\|bios"; then
                info "BIOS/UEFI firmware update detected."
                if [[ "${AC_ONLINE}" -ne 1 ]]; then
                    warn "Skipping BIOS update — AC power required."
                    warn "Connect AC and run: sudo fwupdmgr update"
                else
                    info "Applying BIOS update (will apply on next reboot)..."
                    fwupdmgr update -y || warn "Firmware update failed."
                    info "IMPORTANT: Reboot required to apply BIOS/UEFI capsule update."
                fi
            else
                info "Applying non-BIOS firmware updates..."
                fwupdmgr update -y || warn "One or more firmware updates failed."
            fi
        fi
    fi

fi

# ===============================================================================
# SECTION 11: Automatic Updates
#
# Bazzite's OS updates are atomic via rpm-ostree. The rpm-ostreed service
# can stage updates automatically; they apply on reboot (never mid-session).
#
# Flatpak updates are managed by Bazzite's bazzite-flatpak-manager on boot,
# and can also be triggered via `flatpak update` or `ujust update`.
# ===============================================================================

step "Automatic Updates Configuration"

# Enable rpm-ostree automatic update staging
OSTREE_CONF="/etc/rpm-ostreed.conf"
if [[ -f "${OSTREE_CONF}" ]]; then
    # Set AutomaticUpdatePolicy to 'stage' — downloads and stages updates
    # automatically. They are applied on the next user-initiated reboot.
    if grep -q "^AutomaticUpdatePolicy=" "${OSTREE_CONF}"; then
        sed -i 's/^AutomaticUpdatePolicy=.*/AutomaticUpdatePolicy=stage/' "${OSTREE_CONF}"
    else
        echo "AutomaticUpdatePolicy=stage" >> "${OSTREE_CONF}"
    fi
    info "rpm-ostree AutomaticUpdatePolicy set to 'stage'."
else
    # Bazzite may put this in /usr/etc/ (immutable). Write to /etc/.
    mkdir -p /etc/rpm-ostreed.conf.d 2>/dev/null || true
    cat > /etc/rpm-ostreed.conf <<EOF
[Daemon]
AutomaticUpdatePolicy=stage
EOF
    info "Created /etc/rpm-ostreed.conf with AutomaticUpdatePolicy=stage."
fi

# Enable the rpm-ostreed update timer
systemctl enable --now rpm-ostreed-automatic.timer 2>/dev/null \
    || warn "rpm-ostreed-automatic.timer not found — updates will stage on login."

# Flatpak: Bazzite handles this at boot, but ensure the background update
# service is enabled for user-session Flatpaks
systemctl --user enable --now flatpak-system-update.timer 2>/dev/null \
    || info "Flatpak auto-update timer: managed by Bazzite (bazzite-flatpak-manager)."

info "Automatic update configuration complete."
info "OS updates stage silently and apply on next reboot."
info "Run 'ujust update' at any time to manually check and stage updates."

# ===============================================================================
# SECTION 12: MOTD
# ===============================================================================

step "MOTD Configuration"

info "Configuring MOTD system information display..."

# Bazzite uses pam_motd; drop scripts in /etc/update-motd.d/
mkdir -p /etc/update-motd.d/
rm -f /etc/update-motd.d/*

cat > /etc/update-motd.d/80-sysinfo <<'EOF'
#!/bin/bash
echo "=== System Information ==="
echo ""
echo "  🖥️  Operating System : $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')"
echo "  🌐  IPv4 Address     : $(hostname -I 2>/dev/null | awk '{print $1}')"
echo "  🔗  IPv6 Global      : $(ip -6 addr show scope global 2>/dev/null | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)"
echo "  🪢  IPv6 Link-local  : $(ip -6 addr show scope link  2>/dev/null | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)"
echo "  ⏱️  System Uptime    : $(uptime -p 2>/dev/null)"
echo "  📦  rpm-ostree       : $(rpm-ostree status --booted 2>/dev/null | grep Version | head -n1 | awk '{print $2}' || echo 'unknown')"
echo ""
EOF

chmod +x /etc/update-motd.d/80-sysinfo

# ===============================================================================
# SECTION 13: Final Diagnostics
# ===============================================================================

step "Final Diagnostics"

DIAG_PASS=0
DIAG_WARN=0
DIAG_FAIL=0

diag_ok()   { echo -e "  \033[0;32m[PASS]\033[0m $*"; (( DIAG_PASS++ )); }
diag_warn() { echo -e "  \033[0;33m[WARN]\033[0m $*"; (( DIAG_WARN++ )); }
diag_fail() { echo -e "  \033[0;31m[FAIL]\033[0m $*"; (( DIAG_FAIL++ )); }

echo ""
echo "  ── rpm-ostree ──────────────────────────────────────────"

if rpm-ostree status &>/dev/null; then
    PENDING=$(rpm-ostree status 2>/dev/null | grep -c "pending" || true)
    if [[ "${PENDING}" -gt 0 ]]; then
        diag_ok  "rpm-ostree has a pending deployment (reboot required to activate)"
    else
        diag_warn "rpm-ostree: no pending deployment detected (layering may have failed)"
    fi
else
    diag_fail "rpm-ostree status failed"
fi

echo ""
echo "  ── Firewall ────────────────────────────────────────────"

if systemctl is-active --quiet firewalld; then
    diag_ok "firewalld is running"
    DEFAULT_ZONE=$(firewall-cmd --get-default-zone 2>/dev/null || echo "unknown")
    if [[ "${DEFAULT_ZONE}" == "drop" ]]; then
        diag_ok "Default zone is 'drop' (default-deny inbound)"
    else
        diag_warn "Default zone is '${DEFAULT_ZONE}' — expected 'drop'"
    fi
else
    diag_fail "firewalld is NOT running"
fi

echo ""
echo "  ── SSH ─────────────────────────────────────────────────"

if [[ -f "${SSHD_CUSTOM_CFG}" ]]; then
    diag_ok "SSH hardened drop-in exists: ${SSHD_CUSTOM_CFG}"
else
    diag_fail "SSH drop-in config NOT found"
fi

if [[ -f "${SSHD_DIR}/ssh_host_ed25519_key" ]]; then
    diag_ok "ED25519 host key present"
else
    diag_warn "ED25519 host key NOT found (normal if openssh-server pending reboot)"
fi

if systemctl is-active --quiet sshd 2>/dev/null; then
    diag_ok "sshd is running"
else
    diag_warn "sshd is NOT running (expected — activate after rpm-ostree reboot)"
fi

echo ""
echo "  ── UDM SE Certificate ──────────────────────────────────"

if [[ -f "${CERT_DST}" ]]; then
    diag_ok "UDM SE certificate installed: ${CERT_DST}"
else
    diag_warn "UDM SE certificate NOT installed (check connectivity to ghost.baden.braedach.com)"
fi

echo ""
echo "  ── ClamAV Container ────────────────────────────────────"

if command -v distrobox &>/dev/null; then
    if distrobox list --root 2>/dev/null | grep -q "clamav-box"; then
        diag_ok "clamav-box Distrobox container exists"
    else
        diag_warn "clamav-box container not found (may still be initializing)"
    fi
    if [[ -x /usr/local/bin/clamscan-host ]]; then
        diag_ok "clamscan-host wrapper installed"
    else
        diag_warn "clamscan-host wrapper NOT found"
    fi
else
    diag_warn "Distrobox not available — ClamAV container check skipped"
fi

echo ""
echo "  ── Insync ──────────────────────────────────────────────"

if [[ -f /etc/yum.repos.d/insync.repo ]]; then
    diag_ok "Insync RPM repo configured: /etc/yum.repos.d/insync.repo"
else
    diag_warn "Insync RPM repo NOT found"
fi

echo ""
echo "  ── Flatpak Apps ────────────────────────────────────────"

for app in org.gimp.GIMP org.inkscape.Inkscape org.kde.krita com.google.Chrome org.libreoffice.LibreOffice; do
    if flatpak info "${app}" &>/dev/null; then
        diag_ok "${app}"
    else
        diag_warn "${app} — not installed (may have failed)"
    fi
done

echo ""
echo "  ── IPv6 Configuration ──────────────────────────────────"

if [[ -f /etc/sysctl.d/99-ipv6-laptop.conf ]]; then
    diag_ok "IPv6 stable address sysctl installed"
else
    diag_warn "IPv6 sysctl config not found"
fi

echo ""
echo "  ── Auto-Updates ────────────────────────────────────────"

if grep -q "AutomaticUpdatePolicy=stage" /etc/rpm-ostreed.conf 2>/dev/null; then
    diag_ok "rpm-ostree AutomaticUpdatePolicy=stage configured"
else
    diag_warn "rpm-ostree auto-update policy not confirmed"
fi

echo ""
echo "  ── Systemd Failed Units ────────────────────────────────"

FAILED_UNITS=$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [[ -z "${FAILED_UNITS}" ]]; then
    diag_ok "No failed systemd units detected"
else
    while IFS= read -r unit; do
        diag_warn "Failed unit: ${unit}"
    done <<< "${FAILED_UNITS}"
fi

echo ""
echo "════════════════════════════════════════════════════════════"
echo "  DIAGNOSTIC SUMMARY"
echo "  Pass: ${DIAG_PASS}  |  Warn: ${DIAG_WARN}  |  Fail: ${DIAG_FAIL}"
echo "════════════════════════════════════════════════════════════"

# ===============================================================================
# POST-SCRIPT INSTRUCTIONS
# ===============================================================================

cat <<'POSTINSTALL'

════════════════════════════════════════════════════════════════
  IMPORTANT POST-SCRIPT ACTIONS
════════════════════════════════════════════════════════════════

1. REBOOT REQUIRED
   rpm-ostree packages (wireguard-tools, insync, openssh-server,
   fwupd, etc.) are staged and will activate on next reboot.
   Run:  systemctl reboot
   Or just reboot normally.

2. AFTER REBOOT — Complete printer setup (if CUPS wasn't running):
   sudo lpadmin -p EPSON-WF4830 -E \
     -v ipp://192.168.1.11/ipp/print \
     -m everywhere -D "EPSON WF-4830 Series"

3. AFTER REBOOT — Set up Insync:
   Open the Insync GUI from your application launcher.
   Authenticate your Google account(s) and configure sync folders.
   Insync is a paid app ($29.99/Google account, 15-day trial).

4. WAYDROID (Android / Google Play Store):
   Bazzite has native Waydroid support. After reboot, run:
     ujust setup-waydroid
   This launches an interactive setup wizard in the terminal.
   Follow the prompts to enable Android container and Google Play.
   ARM translation (for most apps) is included automatically.
   NOTE: This CANNOT be automated — it requires graphical interaction.

5. WIREGUARD VPN:
   WireGuard kernel support is built into Bazzite's kernel.
   The 'wg' and 'wg-quick' CLI tools are now layered.
   To connect using a config file:
     sudo wg-quick up /etc/wireguard/wg0.conf
   Or use Settings > Network > VPN to add a WireGuard connection.

6. CLAMAV SCANNING:
   Scan your home directory:
     clamscan-host /home/YOUR_USERNAME
   Update definitions manually:
     sudo systemctl start clamav-update.service

7. MANUAL SYSTEM UPDATE:
   ujust update

8. VERIFY SECURE BOOT / SIGNED IMAGE:
   ujust verify-image

════════════════════════════════════════════════════════════════

POSTINSTALL

info "Setup complete. Log: ${LOGFILE}"
info "SSH backup: ${BACKUP_DIR}"
echo ""

# Optional automatic reboot prompt
read -r -t 30 -p "[?] Reboot now to activate layered packages? [y/N] (auto-N in 30s): " REBOOT_CONFIRM || true
if [[ "${REBOOT_CONFIRM,,}" == "y" ]]; then
    info "Rebooting in 5 seconds..."
    sleep 5
    systemctl reboot
else
    info "Reboot skipped. Please reboot manually when ready."
fi

#endscript

Hope this helps someone. Probaby not a wise idea to share the internal network domains but if I get hacked it will get reported to Ubiquiti for the betterment of everyone else.

#enoughsaid