#!/usr/bin/env bash
# A small and opinionated continuous integration tool for projects that do not have the hardware budget or operational staff for large CI/CD systems.
# Made to run locally or in Designed for Hostsharing Container Server environments with Docker (Podman not tested yet).
#
# Create the config file with (--env), edit and source,
# then run the script in the root of a git working tree, e.g. with --install.
# The script waits for new commits on any branch on origin, then checks it out, and runs a command to build+test the branch.
# Call with -h or --help for more details.
#
# This script was mostly vibe-coded to replace the clumsy configuration by code of Jenkins.
#
# MIT License
#
# Copyright (c) 2026 Michael Hönnig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

script_version="0.7.6"
script_path=$(realpath "${BASH_SOURCE[0]}")
script_name=$(basename "${BASH_SOURCE[0]}")
tool_name="GitTally"
installed_script_path=
repo_root=
systemd_unit_name="gitTally.service"
monitor_generation=$(date +%s)

default_build_command='./gradlew --console=plain --no-daemon --no-build-cache --rerun-tasks test'
default_build_clean_command='rm -rf build'
default_build_artefact_dirs='build/reports'
default_build_stdout_log='build.stdout.log'
default_build_stderr_log='build.stderr.log'
default_build_docker_image='hsadmin-ng-build-env:latest'
default_build_dockerfile='Jenkins/jenkins-agent/Dockerfile'
default_build_docker_context='Jenkins/jenkins-agent'
default_build_docker_network='host'
default_build_docker_preflight_command='docker version'
default_build_docker_env='TESTCONTAINERS_RYUK_DISABLED=false'
default_build_docker_java_tool_options='-Ddocker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy -Dtestcontainers.docker.socket.override=/var/run/docker.sock'
default_new_branch_commit_max_age='5d'
default_auto_build_times='02:00'
default_gitea_status_context='GitTally'
default_gitea_base_url='https://git.example.org'
default_gitea_owner='example-owner'
default_gitea_repo='example-repo'
default_gitea_git_username='example-user'
default_artifact_auth_gitea_base_url='https://git.example.org'
default_artifact_auth_image='quay.io/oauth2-proxy/oauth2-proxy:v7.13.0'
default_artifact_auth_http_port='4180'
default_artifact_auth_email_domains='*'
default_artifact_public_base_url='https://ci.example.org/'
default_impressum_url='https://example.org/imprint.html'
default_artifact_nginx_server_name='ci.example.org'
default_artifact_nginx_upstream_host='ci.example.org'
default_artifact_nginx_container_name='gittally-nginx-example-repo'
default_artifact_letsencrypt_email='admin@example.org'
default_artifact_auth_client_id='gitea-oauth-client-id'
default_artifact_auth_client_secret='gitea-oauth-client-secret'
default_artifact_auth_cookie_secret='generate-a-random-cookie-secret'
default_artifact_auth_cookie_domain='ci.example.org'
default_artifact_auth_container_name='gittally-auth-example-repo'

config_file_var() {
    local name="$1"

    printf 'GITTALLY_CONFIG_%s' "$name"
}

load_repo_config() {
    local config_file
    local before_vars
    local after_vars
    local env_name
    local config_name
    local backup_name

    repo_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
    if [ -z "$repo_root" ]; then
        return 0
    fi

    config_file="$repo_root/.gitTally"
    if [ ! -f "$config_file" ]; then
        return 0
    fi

    before_vars=$(compgen -v GITTALLY_ | sort)
    while IFS= read -r env_name; do
        if [ -z "$env_name" ]; then
            continue
        fi
        backup_name="__gittally_env_backup_$env_name"
        printf -v "$backup_name" '%s' "${!env_name}"
    done <<<"$before_vars"

    set -a
    # shellcheck source=/dev/null
    . "$config_file"
    set +a
    after_vars=$(compgen -v GITTALLY_ | sort)

    while IFS= read -r env_name; do
        if [ -z "$env_name" ]; then
            continue
        fi
        config_name=$(config_file_var "$env_name")
        printf -v "$config_name" '%s' "${!env_name}"
        if grep -Fxq "$env_name" <<<"$before_vars"; then
            backup_name="__gittally_env_backup_$env_name"
            printf -v "$env_name" '%s' "${!backup_name}"
            unset "$backup_name"
        else
            unset "$env_name"
        fi
    done <<<"$after_vars"
}

config_value() {
    local primary_name="$1"
    local fallback_name="$2"
    local default_value="$3"
    local config_name

    if [ -n "$primary_name" ] && [ -n "${!primary_name+x}" ]; then
        printf '%s' "${!primary_name}"
    elif [ -n "$fallback_name" ] && [ -n "${!fallback_name+x}" ]; then
        printf '%s' "${!fallback_name}"
    elif [ -n "$primary_name" ]; then
        config_name=$(config_file_var "$primary_name")
        if [ -n "${!config_name+x}" ]; then
            printf '%s' "${!config_name}"
        else
            printf '%s' "$default_value"
        fi
    else
        printf '%s' "$default_value"
    fi
}

branch_config_value() {
    local primary_name="$1"
    local fallback_name="$2"
    local checkout_repo_root
    local config_file
    local value_file
    local status

    checkout_repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
    config_file="$checkout_repo_root/.gitTally"
    if [ ! -f "$config_file" ]; then
        return 1
    fi

    value_file=$(mktemp "${TMPDIR:-/tmp}/gittally-config-value.XXXXXX") || return 1
    (
        unset "$primary_name"
        if [ -n "$fallback_name" ]; then
            unset "$fallback_name"
        fi
        set -a
        # shellcheck source=/dev/null
        . "$config_file" >/dev/null
        set +a
        if [ -n "${!primary_name+x}" ]; then
            printf '%s' "${!primary_name}" >"$value_file"
        elif [ -n "$fallback_name" ] && [ -n "${!fallback_name+x}" ]; then
            printf '%s' "${!fallback_name}" >"$value_file"
        else
            exit 1
        fi
    )
    status=$?

    if [ "$status" -eq 0 ]; then
        cat "$value_file"
    fi
    rm -f "$value_file"
    return "$status"
}

install_target_dir() {
    local target_dir

    target_dir=$(config_value GITTALLY_INSTALL_DIR "" "$HOME/bin")
    if [ -z "$target_dir" ]; then
        echo "ERROR: GITTALLY_INSTALL_DIR must not be empty." >&2
        return 1
    fi

    printf '%s' "$target_dir"
}

systemd_quote() {
    local value="$1"

    value="${value//\\/\\\\}"
    value="${value//\"/\\\"}"
    value="${value//%/%%}"
    printf '"%s"' "$value"
}

systemd_path() {
    local value="$1"

    value="${value//%/%%}"
    printf '%s' "$value"
}

generate_systemd_config() {
    local target_dir="$1"
    local target_path="$2"
    local service_path="$target_dir/$systemd_unit_name"
    local env_path="$target_dir/gitTally.env"
    local working_dir="${repo_root:-$(pwd)}"
    local service_description="GitTally CI for $(basename "$working_dir")"

    cat >"$service_path" <<EOF
[Unit]
Description=$service_description
Wants=network-online.target
After=network-online.target docker.service

[Service]
Type=simple
WorkingDirectory=$(systemd_path "$working_dir")
EnvironmentFile=-$(systemd_path "$env_path")
ExecStart=/usr/bin/env bash $(systemd_quote "$target_path") --nginx --docker
Restart=always
RestartSec=30

[Install]
WantedBy=default.target
EOF
    echo "generated $service_path"

    if [ -f "$env_path" ]; then
        echo "kept existing $env_path"
        return 0
    fi

    cat >"$env_path" <<EOF
# EnvironmentFile for $tool_name systemd service.
# Values here override environment values loaded from the repository .gitTally file.
GITTALLY_GITEA_GIT_USERNAME=$(shell_quote "$gitea_git_username")
GITTALLY_GITEA_TOKEN=
EOF
    chmod 600 "$env_path"
    echo "generated $env_path"
}

install_systemd_service() {
    local service_path="$1"
    local user_systemd_dir="$HOME/.config/systemd/user"
    local since_ts

    mkdir -p "$user_systemd_dir"
    ln -sf "$service_path" "$user_systemd_dir/$systemd_unit_name"
    systemctl --user daemon-reload
    systemctl --user enable "$systemd_unit_name"
    since_ts=$(date -Iseconds)
    systemctl --user restart "$systemd_unit_name"
    echo "installed systemd user service: $systemd_unit_name"
    echo "waiting for service to be ready..."
    timeout 120 journalctl --user -u "$systemd_unit_name" --since="$since_ts" --follow --output=cat --no-pager 2>/dev/null | \
        while IFS= read -r line; do
            echo "  | $line"
            [[ "$line" == *"$tool_name version"*": service ready"* ]] && break
        done || true
}

run_systemd_action() {
    local action="$1"

    case "$action" in
        start|stop|status|enable|disable)
            systemctl --user "$action" "$systemd_unit_name"
            ;;
        reload)
            systemctl --user daemon-reload
            systemctl --user restart "$systemd_unit_name"
            ;;
        log)
            journalctl --user -u "$systemd_unit_name" -b --no-pager
            ;;
        watch)
            journalctl --user -u "$systemd_unit_name" -f
            ;;
        *)
            echo "ERROR: unknown systemd action: $action" >&2
            echo "Supported actions: start, stop, reload, status, log, watch, enable, disable." >&2
            return 1
            ;;
    esac
}

generate_update_script() {
    local target_dir="$1"
    local target_path="$2"
    local install_branch="$3"
    local update_script_path="$target_dir/gitTally-update"
    local env_path="$target_dir/gitTally.env"
    local working_dir="${repo_root:-$(pwd)}"

    cat >"$update_script_path" <<EOF
#!/usr/bin/env bash
set -euo pipefail

env_file=$(shell_quote "$env_path")
install_dir=$(shell_quote "$target_dir")
repo_root=$(shell_quote "$working_dir")
install_branch=$(shell_quote "$install_branch")
installed_git_tally=$(shell_quote "$target_path")

if [ -f "\$env_file" ]; then
    set -a
    # shellcheck source=/dev/null
    . "\$env_file"
    set +a
fi

export GITTALLY_INSTALL_DIR="\${GITTALLY_INSTALL_DIR:-\$install_dir}"

if [ -z "\$install_branch" ] || [ "\$install_branch" = "detached HEAD" ]; then
    echo "ERROR: cannot update $tool_name because the install source branch is not known." >&2
    exit 1
fi

cd "\$repo_root"
git switch "\$install_branch"
tools/gitTally --pull --install --systemd
EOF
    chmod 700 "$update_script_path"
    echo "generated $update_script_path"
}

install_to_bin() {
    local target_dir
    local target_path
    local install_branch

    target_dir=$(install_target_dir) || exit 1
    mkdir -p "$target_dir"
    target_dir=$(realpath "$target_dir")
    target_path="$target_dir/$script_name"
    cp "$script_path" "$target_path"
    chmod +x "$target_path"
    install_branch=$(git -C "$(dirname "$script_path")" branch --show-current 2>/dev/null)
    if [ -z "$install_branch" ]; then
        install_branch="detached HEAD"
    fi
    generate_systemd_config "$target_dir" "$target_path"
    generate_update_script "$target_dir" "$target_path" "$install_branch"
    echo "installed $target_path from branch: $install_branch"
    installed_script_path="$target_path"
}

shell_quote() {
    printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}

origin_url() {
    git remote get-url origin 2>/dev/null || true
}

detect_git_username_from_origin_url() {
    local origin_url
    local authority

    origin_url=$(origin_url)
    authority="${origin_url#*://}"
    authority="${authority%%/*}"
    if [[ "$authority" == *@* ]]; then
        echo "${authority%@*}"
    fi
}

detect_gitea_repo_from_origin_url() {
    local origin_url
    local path

    origin_url=$(origin_url)
    if [ -z "$origin_url" ]; then
        return 0
    fi

    if [[ "$origin_url" =~ ^https?:// ]]; then
        if [ -z "$gitea_base_url" ]; then
            gitea_base_url=$(printf '%s' "$origin_url" | sed -E 's#^(https?://)([^/@]+@)?([^/]+)/.*#\1\3#')
        fi
        path=$(printf '%s' "$origin_url" | sed -E 's#^https?://[^/]+/##')
    elif [[ "$origin_url" == git@*:* ]]; then
        if [ -z "$gitea_base_url" ]; then
            gitea_base_url="https://${origin_url#git@}"
            gitea_base_url="${gitea_base_url%%:*}"
        fi
        path="${origin_url#*:}"
    else
        return 0
    fi

    path="${path%.git}"
    if [ -z "$gitea_owner" ]; then
        gitea_owner="${path%%/*}"
    fi
    if [ -z "$gitea_repo" ] && [[ "$path" == */* ]]; then
        gitea_repo="${path#*/}"
    fi
}

print_env_section() {
    local title="$1"

    printf '\n'
    printf '# %s\n' "================================================================================"
    printf '# %s\n' "$title"
    printf '# %s\n' "--------------------------------------------------------------------------------"
}

is_sensitive_var_name() {
    case "$1" in
        *_TOKEN*|*_SECRET*|*_PASSWORD*|*_APIKEY*|*_PASSKEY*|*_USERNAME*) return 0 ;;
    esac
    return 1
}

print_env_var() {
    local name="$1"
    local value="$2"
    local comment="$3"
    local default_value="${4:-}"
    local display_value="$value"

    if is_sensitive_var_name "$name" && [ -n "$value" ]; then
        display_value='<secret-value-hidden>'
    fi

    printf '\n'
    printf '# %s\n' "$comment"
    printf '# default %s=%s\n' "$name" "$(shell_quote "$default_value")"
    printf 'export %s=%s\n' "$name" "$(shell_quote "$display_value")"
}

print_env_optional_var() {
    local name="$1"
    local value="$2"
    local comment="$3"
    local default_value="${4:-}"
    local display_value="$value"

    if is_sensitive_var_name "$name" && [ -n "$value" ]; then
        display_value='secret-value-hidden'
    fi

    printf '\n'
    printf '# %s\n' "$comment"
    printf '# default %s=%s\n' "$name" "$(shell_quote "$default_value")"
    if [ -n "$value" ]; then
        printf '# resolved %s=%s\n' "$name" "$(shell_quote "$display_value")"
    fi
    printf '# export %s=%s\n' "$name" "$(shell_quote "")"
}

print_env() {
    local install_dir
    local build_command
    local build_clean_command
    local build_artefact_dirs
    local build_stdout_log
    local build_stderr_log
    local build_docker_image
    local build_dockerfile
    local build_docker_context
    local build_docker_network
    local build_docker_preflight_command
    local build_docker_env
    local build_docker_java_tool_options
    local new_branch_commit_max_age
    local artifact_server_port
    local artifact_server_bind_address
    local artifact_server_host
    local artifact_public_base_url
    local artifact_build_retention_per_branch
    local artifact_nginx_server_name
    local artifact_nginx_http_port
    local artifact_nginx_https_port
    local artifact_nginx_upstream_host
    local artifact_nginx_container_name
    local artifact_nginx_state_dir
    local artifact_letsencrypt_email
    local artifact_certbot_env
    local artifact_auth_mode
    local artifact_auth_gitea_base_url
    local artifact_auth_client_id
    local artifact_auth_client_secret
    local artifact_auth_cookie_secret
    local artifact_auth_cookie_domain
    local artifact_auth_image
    local artifact_auth_container_name
    local artifact_auth_http_port
    local artifact_auth_email_domains
    local impressum_url
    local gitea_token
    local gitea_status_context
    local gitea_status_target_url
    local default_artifact_nginx_state_dir
    local default_repository_key
    local default_repository_simple_name

    install_dir=$(config_value GITTALLY_INSTALL_DIR "" "$HOME/bin")
    build_command=$(config_value GITTALLY_BUILD_COMMAND "" "$default_build_command")
    build_clean_command=$(config_value GITTALLY_BUILD_CLEAN_COMMAND "" "$default_build_clean_command")
    build_artefact_dirs=$(config_value GITTALLY_BUILD_ARTEFACT_DIRS "" "$default_build_artefact_dirs")
    build_stdout_log=$(config_value GITTALLY_BUILD_STDOUT_LOG "" "$default_build_stdout_log")
    build_stderr_log=$(config_value GITTALLY_BUILD_STDERR_LOG "" "$default_build_stderr_log")
    build_docker_image=$(config_value GITTALLY_BUILD_DOCKER_IMAGE HSADMIN_NG_BUILD_IMAGE "$default_build_docker_image")
    build_dockerfile=$(config_value GITTALLY_BUILD_DOCKERFILE "" "$default_build_dockerfile")
    build_docker_context=$(config_value GITTALLY_BUILD_DOCKER_CONTEXT "" "$default_build_docker_context")
    build_docker_network=$(config_value GITTALLY_BUILD_DOCKER_NETWORK HSADMIN_NG_BUILD_NETWORK "$default_build_docker_network")
    build_docker_preflight_command=$(config_value GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND "" "$default_build_docker_preflight_command")
    build_docker_env=$(config_value GITTALLY_BUILD_DOCKER_ENV "" "$default_build_docker_env")
    build_docker_java_tool_options=$(config_value GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS "" "$default_build_docker_java_tool_options")
    new_branch_commit_max_age=$(config_value GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE "" "$default_new_branch_commit_max_age")
    auto_build_branches=$(config_value GITTALLY_AUTO_BUILD_BRANCHES "" "")
    auto_build_times=$(config_value GITTALLY_AUTO_BUILD_TIMES "" "$default_auto_build_times")

    artifact_server_port=$(config_value GITTALLY_ARTIFACT_SERVER_PORT HSADMIN_NG_ARTIFACT_SERVER_PORT 18080)
    artifact_server_bind_address=$(config_value GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS HSADMIN_NG_ARTIFACT_SERVER_BIND_ADDRESS 0.0.0.0)
    artifact_server_host=$(config_value GITTALLY_ARTIFACT_SERVER_HOST HSADMIN_NG_ARTIFACT_SERVER_HOST "")
    artifact_http_server_port="$artifact_server_port"
    artifact_http_server_bind_address="$artifact_server_bind_address"
    artifact_http_server_host="$artifact_server_host"
    artifact_nginx_server_name=$(config_value GITTALLY_ARTIFACT_NGINX_SERVER_NAME HSADMIN_NG_ARTIFACT_NGINX_SERVER_NAME "")
    artifact_public_base_url=$(config_value GITTALLY_ARTIFACT_PUBLIC_BASE_URL HSADMIN_NG_ARTIFACT_PUBLIC_BASE_URL "")
    artifact_build_retention_per_branch=$(config_value GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH HSADMIN_NG_ARTIFACT_BUILD_RETENTION_PER_BRANCH 3)
    artifact_nginx_http_port=$(config_value GITTALLY_ARTIFACT_NGINX_HTTP_PORT HSADMIN_NG_ARTIFACT_NGINX_HTTP_PORT 8080)
    artifact_nginx_https_port=$(config_value GITTALLY_ARTIFACT_NGINX_HTTPS_PORT HSADMIN_NG_ARTIFACT_NGINX_HTTPS_PORT 8443)
    artifact_nginx_upstream_host=$(config_value GITTALLY_ARTIFACT_NGINX_UPSTREAM_HOST HSADMIN_NG_ARTIFACT_NGINX_UPSTREAM_HOST "")
    artifact_nginx_container_name=$(config_value GITTALLY_ARTIFACT_NGINX_CONTAINER_NAME HSADMIN_NG_ARTIFACT_NGINX_CONTAINER_NAME "")
    artifact_nginx_state_dir=$(config_value GITTALLY_ARTIFACT_NGINX_STATE_DIR HSADMIN_NG_ARTIFACT_NGINX_STATE_DIR "")
    artifact_letsencrypt_email=$(config_value GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL HSADMIN_NG_ARTIFACT_LETSENCRYPT_EMAIL "")
    artifact_certbot_env=$(config_value GITTALLY_ARTIFACT_CERTBOT_ENV HSADMIN_NG_ARTIFACT_CERTBOT_ENV "")
    artifact_auth_mode=$(config_value GITTALLY_ARTIFACT_AUTH_MODE "" "")
    artifact_auth_gitea_base_url=$(config_value GITTALLY_ARTIFACT_AUTH_GITEA_BASE_URL "" "")
    artifact_auth_client_id=$(config_value GITTALLY_ARTIFACT_AUTH_CLIENT_ID "" "")
    artifact_auth_client_secret=$(config_value GITTALLY_ARTIFACT_AUTH_CLIENT_SECRET "" "")
    artifact_auth_cookie_secret=$(config_value GITTALLY_ARTIFACT_AUTH_COOKIE_SECRET "" "")
    artifact_auth_cookie_domain=$(config_value GITTALLY_ARTIFACT_AUTH_COOKIE_DOMAIN "" "")
    artifact_auth_image=$(config_value GITTALLY_ARTIFACT_AUTH_IMAGE "" "$default_artifact_auth_image")
    artifact_auth_container_name=$(config_value GITTALLY_ARTIFACT_AUTH_CONTAINER_NAME "" "")
    artifact_auth_http_port=$(config_value GITTALLY_ARTIFACT_AUTH_HTTP_PORT "" "$default_artifact_auth_http_port")
    artifact_auth_email_domains=$(config_value GITTALLY_ARTIFACT_AUTH_EMAIL_DOMAINS "" "$default_artifact_auth_email_domains")
    impressum_url=$(config_value GITTALLY_IMPRESSUM_URL "" "$default_impressum_url")
    if [ -n "$artifact_public_base_url" ]; then
        :
    elif [ -n "$artifact_nginx_server_name" ]; then
        artifact_public_base_url="https://$artifact_nginx_server_name/"
    elif [ -n "$artifact_server_host" ]; then
        artifact_public_base_url="http://$artifact_server_host:$artifact_server_port/"
    else
        artifact_public_base_url="http://$artifact_server_bind_address:$artifact_server_port/"
    fi
    if [[ "$artifact_public_base_url" != */ ]]; then
        artifact_public_base_url="$artifact_public_base_url/"
    fi

    gitea_base_url=$(config_value GITTALLY_GITEA_BASE_URL HSADMIN_NG_GITEA_BASE_URL "")
    gitea_owner=$(config_value GITTALLY_GITEA_OWNER HSADMIN_NG_GITEA_OWNER "")
    gitea_repo=$(config_value GITTALLY_GITEA_REPO HSADMIN_NG_GITEA_REPO "")
    gitea_git_username=$(config_value GITTALLY_GITEA_GIT_USERNAME HSADMIN_NG_GITEA_GIT_USERNAME "")
    gitea_token=$(config_value GITTALLY_GITEA_TOKEN HSADMIN_NG_GITEA_TOKEN "")
    gitea_status_context=$(config_value GITTALLY_GITEA_STATUS_CONTEXT HSADMIN_NG_GITEA_STATUS_CONTEXT "$default_gitea_status_context")
    gitea_status_target_url=$(config_value GITTALLY_GITEA_STATUS_TARGET_URL HSADMIN_NG_GITEA_STATUS_TARGET_URL "")
    detect_gitea_repo_from_origin_url
    if [ -z "$gitea_git_username" ]; then
        gitea_git_username=$(detect_git_username_from_origin_url)
    fi
    if [ -z "$artifact_auth_gitea_base_url" ]; then
        artifact_auth_gitea_base_url="${gitea_base_url:-$default_artifact_auth_gitea_base_url}"
    fi
    default_repository_key=$(printf '%s' "${repo_root:-$(git rev-parse --show-toplevel)}" | sed 's#[^[:alnum:]._-]#_#g')
    default_repository_simple_name=$(basename "${repo_root:-$(git rev-parse --show-toplevel)}")
    default_artifact_nginx_state_dir='${XDG_STATE_HOME:-$HOME/.local/state}/gittally/nginx/<repo-key>'
    if [ -z "$artifact_nginx_upstream_host" ] && [ -n "$artifact_nginx_server_name" ]; then
        artifact_nginx_upstream_host="$artifact_nginx_server_name"
    fi
    if [ -z "$artifact_nginx_container_name" ]; then
        artifact_nginx_container_name="gittally-nginx-$(printf '%s' "$default_repository_simple_name" | sed 's#[^[:alnum:]_.-]#-#g')"
    fi
    if [ -z "$artifact_nginx_state_dir" ]; then
        artifact_nginx_state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/gittally/nginx/$default_repository_key"
    fi
    if [ -z "$artifact_auth_container_name" ]; then
        artifact_auth_container_name="gittally-auth-$(printf '%s' "$default_repository_simple_name" | sed 's#[^[:alnum:]_.-]#-#g')"
    fi

    printf '# Environment for %s version %s\n' "$tool_name" "$script_version"
    printf '# Save and source this output before starting the script, for example:\n'
    printf '#   %s --env > .gittally.env\n' "$script_name"
    printf '#   . .gittally.env\n'

    print_env_section "Installation"
    print_env_var GITTALLY_INSTALL_DIR "$install_dir" 'Target directory used by --install.' "$HOME/bin"

    print_env_section "Build command"
    print_env_var GITTALLY_BUILD_COMMAND "$build_command" 'Fallback shell command used if the checked-out branch .gitTally does not define it. The branch name is available as $branch.' "$default_build_command"
    print_env_var GITTALLY_BUILD_CLEAN_COMMAND "$build_clean_command" 'Shell command executed before a non-Docker build and before preparing a Docker workspace.' "$default_build_clean_command"
    print_env_var GITTALLY_BUILD_ARTEFACT_DIRS "$build_artefact_dirs" "Report directories copied into artifacts. Separate multiple paths with ';'." "$default_build_artefact_dirs"
    print_env_var GITTALLY_BUILD_STDOUT_LOG "$build_stdout_log" 'Artifact filename for captured build stdout.' "$default_build_stdout_log"
    print_env_var GITTALLY_BUILD_STDERR_LOG "$build_stderr_log" 'Artifact filename for captured build stderr.' "$default_build_stderr_log"
    print_env_var GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE "$new_branch_commit_max_age" 'Maximum age for the latest commit on new origin branches. Use h/d suffix.' "$default_new_branch_commit_max_age"

    print_env_section "Docker build runtime"
    print_env_var GITTALLY_BUILD_DOCKER_IMAGE "$build_docker_image" 'Docker image used when --docker is enabled.' "$default_build_docker_image"
    print_env_var GITTALLY_BUILD_DOCKERFILE "$build_dockerfile" 'Dockerfile used to build the image when it does not exist locally.' "$default_build_dockerfile"
    print_env_var GITTALLY_BUILD_DOCKER_CONTEXT "$build_docker_context" 'Docker build context used with GITTALLY_BUILD_DOCKERFILE.' "$default_build_docker_context"
    print_env_var GITTALLY_BUILD_DOCKER_NETWORK "$build_docker_network" 'Docker network mode for build containers.' "$default_build_docker_network"
    print_env_var GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND "$build_docker_preflight_command" 'Command run inside the build container to verify Docker access.' "$default_build_docker_preflight_command"
    print_env_var GITTALLY_BUILD_DOCKER_ENV "$build_docker_env" 'Additional environment assignments passed to the build container, separated by spaces.' "$default_build_docker_env"
    print_env_var GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS "$build_docker_java_tool_options" 'Java tool options added for Docker and Testcontainers defaults.' "$default_build_docker_java_tool_options"

    print_env_section "Artifact server"
    print_env_var GITTALLY_ARTIFACT_SERVER_PORT "$artifact_server_port" 'Preferred HTTP port for serving archived build artifacts.' 18080
    print_env_var GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS "$artifact_server_bind_address" 'Bind address for the artifact HTTP server.' 0.0.0.0
    print_env_var GITTALLY_ARTIFACT_SERVER_HOST "$artifact_server_host" 'Host name or address printed in local artifact server URLs.' ""
    print_env_var GITTALLY_ARTIFACT_PUBLIC_BASE_URL "$artifact_public_base_url" 'Public base URL used for artifact links and Gitea status target URLs.' "$default_artifact_public_base_url"
    print_env_var GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH "$artifact_build_retention_per_branch" 'Retained builds per branch. Use a count, or h/d suffix for age based retention.' 3

    print_env_section "Nginx and certificates"
    print_env_var GITTALLY_ARTIFACT_NGINX_SERVER_NAME "$artifact_nginx_server_name" 'Public server name for the nginx and certificate setup.' "$default_artifact_nginx_server_name"
    print_env_var GITTALLY_ARTIFACT_NGINX_HTTP_PORT "$artifact_nginx_http_port" 'Host HTTP port published by the nginx container.' 8080
    print_env_var GITTALLY_ARTIFACT_NGINX_HTTPS_PORT "$artifact_nginx_https_port" 'Host HTTPS port published by the nginx container.' 8443
    print_env_var GITTALLY_ARTIFACT_NGINX_UPSTREAM_HOST "$artifact_nginx_upstream_host" 'Host name nginx uses to reach the artifact HTTP server.' "$default_artifact_nginx_upstream_host"
    print_env_var GITTALLY_ARTIFACT_NGINX_CONTAINER_NAME "$artifact_nginx_container_name" 'Docker container name for the nginx reverse proxy.' "$default_artifact_nginx_container_name"
    print_env_optional_var GITTALLY_ARTIFACT_NGINX_STATE_DIR "$artifact_nginx_state_dir" 'Persistent state directory for nginx config, logs, and certificate data.' "$default_artifact_nginx_state_dir"
    print_env_var GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL "$artifact_letsencrypt_email" 'Email address used when registering Lets Encrypt certificates.' "$default_artifact_letsencrypt_email"
    print_env_var GITTALLY_ARTIFACT_CERTBOT_ENV "$artifact_certbot_env" 'Additional certbot arguments, for example --staging.' ""

    print_env_section "Artifact website Gitea login"
    print_env_var GITTALLY_ARTIFACT_AUTH_MODE "$artifact_auth_mode" 'Frontend auth mode for --nginx. Use gitea-oauth2 to require a Gitea browser login.' ""
    print_env_var GITTALLY_ARTIFACT_AUTH_GITEA_BASE_URL "$artifact_auth_gitea_base_url" 'Gitea base URL used only for browser login to the artifact website.' "$default_artifact_auth_gitea_base_url"
    print_env_var GITTALLY_ARTIFACT_AUTH_CLIENT_ID "$artifact_auth_client_id" 'OAuth2 client ID registered in Gitea for the artifact website.' "$default_artifact_auth_client_id"
    print_env_var GITTALLY_ARTIFACT_AUTH_CLIENT_SECRET "$artifact_auth_client_secret" 'OAuth2 client secret registered in Gitea for the artifact website.' "$default_artifact_auth_client_secret"
    print_env_var GITTALLY_ARTIFACT_AUTH_COOKIE_SECRET "$artifact_auth_cookie_secret" 'Random oauth2-proxy cookie secret used to protect browser sessions.' "$default_artifact_auth_cookie_secret"
    print_env_var GITTALLY_ARTIFACT_AUTH_COOKIE_DOMAIN "$artifact_auth_cookie_domain" 'Optional cookie domain for browser sessions.' "$default_artifact_auth_cookie_domain"
    print_env_var GITTALLY_ARTIFACT_AUTH_IMAGE "$artifact_auth_image" 'OAuth2 proxy Docker image used for Gitea browser login.' "$default_artifact_auth_image"
    print_env_var GITTALLY_ARTIFACT_AUTH_CONTAINER_NAME "$artifact_auth_container_name" 'Docker container name for the artifact website auth proxy.' "$default_artifact_auth_container_name"
    print_env_var GITTALLY_ARTIFACT_AUTH_HTTP_PORT "$artifact_auth_http_port" 'Internal HTTP port used by the artifact website auth proxy.' "$default_artifact_auth_http_port"
    print_env_var GITTALLY_ARTIFACT_AUTH_EMAIL_DOMAINS "$artifact_auth_email_domains" 'Allowed email domains for logged-in Gitea users. Use * to allow any Gitea login.' "$default_artifact_auth_email_domains"

    print_env_section "Legal"
    print_env_var GITTALLY_IMPRESSUM_URL "$impressum_url" 'URL for the Impressum (Legal Disclosure) link in the footer.' "$default_impressum_url"

    print_env_section "Auto builds"
    print_env_var GITTALLY_AUTO_BUILD_BRANCHES "$auto_build_branches" "Semicolon-separated list of branches to rebuild automatically. Leave empty to disable auto builds." ""
    print_env_var GITTALLY_AUTO_BUILD_TIMES "$auto_build_times" 'Semicolon-separated list of UTC times (HH:MM) at which auto builds are triggered, e.g. 02:00;08:00;14:00;20:00.' "$default_auto_build_times"

    print_env_section "Gitea"
    print_env_var GITTALLY_GITEA_BASE_URL "$gitea_base_url" 'Base URL of the Gitea instance.' "$default_gitea_base_url"
    print_env_var GITTALLY_GITEA_OWNER "$gitea_owner" 'Gitea repository owner.' "$default_gitea_owner"
    print_env_var GITTALLY_GITEA_REPO "$gitea_repo" 'Gitea repository name.' "$default_gitea_repo"
    print_env_var GITTALLY_GITEA_GIT_USERNAME "$gitea_git_username" 'HTTPS git username used with the Gitea token. (required)' "$default_gitea_git_username"
    print_env_var GITTALLY_GITEA_TOKEN "$gitea_token" 'Token used for Gitea commit statuses and HTTPS git authentication. (required)' ""
    print_env_var GITTALLY_GITEA_STATUS_CONTEXT "$gitea_status_context" 'Gitea commit status context published by GitTally.' "$default_gitea_status_context"
    print_env_var GITTALLY_GITEA_STATUS_TARGET_URL "$gitea_status_target_url" 'Fixed status target URL. Leave empty to link to archived build artifacts.' ""
}

if [ "$1" = "--env" ]; then
    load_repo_config
    print_env
    exit 0
fi

has_arg() {
    local wanted="$1"
    shift
    local arg

    for arg in "$@"; do
        if [ "$arg" = "$wanted" ]; then
            return 0
        fi
    done
    return 1
}

is_repo_safe_command() {
    local arg

    if has_arg --install "$@" || has_arg --pull "$@" || has_arg --help "$@" || has_arg -h "$@"; then
        return 0
    fi
    for arg in "$@"; do
        case "$arg" in
            --systemd|--systemd:*)
                return 0
                ;;
        esac
    done
    return 1
}

script_repo_root=$(git -C "$(dirname "$script_path")" rev-parse --show-toplevel 2>/dev/null || true)
if [ -n "$script_repo_root" ] &&
    [ "${GITTALLY_BIN_FORWARD:-${HSADMIN_NG_GIT_WATCH_ORIGIN_AND_TEST_BIN_FORWARD:-}}" != true ] &&
    ! is_repo_safe_command "$@"; then
    echo "ERROR: $tool_name must not be started from within its repository." >&2
    echo "Only --pull and --install are allowed from within the repository." >&2
    echo "Install it first: $script_path --install" >&2
    echo "Then start it from: ${GITTALLY_INSTALL_DIR:-$HOME/bin}/$script_name" >&2
    exit 1
fi
unset HSADMIN_NG_GIT_WATCH_ORIGIN_AND_TEST_BIN_FORWARD
unset GITTALLY_BIN_FORWARD

reported_skipped_new_branches=$(mktemp "${TMPDIR:-/tmp}/gittally-skipped.XXXXXX")
active_build_branch=
active_build_artifact_key=
active_build_started_at=
active_build_pid=
active_build_cancelled=false
git_askpass_file=
artifact_nginx_container_started=false
artifact_auth_container_started=false
artifact_nginx_container_id=
artifact_auth_container_id=

cleanup() {
    local ended_at
    local ended_timestamp
    local build_duration

    if [ -n "${active_build_pid:-}" ]; then
        echo "stopping active build process: $active_build_pid"
        terminate_process_tree "$active_build_pid" TERM
        sleep 2
        if kill -0 "$active_build_pid" >/dev/null 2>&1; then
            terminate_process_tree "$active_build_pid" KILL
        fi
        wait "$active_build_pid" 2>/dev/null || true
        active_build_pid=
        cleanup_stale_build_runtime || true
    fi
    clear_build_cancel_request || true
    if [ -n "${active_build_branch:-}" ]; then
        ended_at=$(date +%s)
        ended_timestamp=$(date -Iseconds)
        build_duration=$(format_build_duration "$((ended_at - active_build_started_at))")
        echo "marking interrupted build: $active_build_branch"
        record_build_result "$active_build_branch" interrupted "$build_duration" "$ended_timestamp" "$active_build_artifact_key" || true
        write_current_build_page "$active_build_branch" interrupted "$ended_timestamp" || true
        active_build_branch=
        active_build_artifact_key=
        active_build_started_at=
    fi
    if [ -n "$git_askpass_file" ]; then
        rm -f "$git_askpass_file"
    fi
    rm -f "$reported_skipped_new_branches"
    if [ -n "$artifact_http_server_pid" ]; then
        kill "$artifact_http_server_pid" >/dev/null 2>&1 || true
        wait "$artifact_http_server_pid" 2>/dev/null || true
    fi
    if [ "${artifact_nginx_container_started:-false}" = true ] && [ -n "${artifact_nginx_container_id:-}" ]; then
        docker rm -f "$artifact_nginx_container_id" >/dev/null 2>&1 || true
        artifact_nginx_container_started=false
        artifact_nginx_container_id=
    fi
    if [ "${artifact_auth_container_started:-false}" = true ] && [ -n "${artifact_auth_container_id:-}" ]; then
        docker rm -f "$artifact_auth_container_id" >/dev/null 2>&1 || true
        artifact_auth_container_started=false
        artifact_auth_container_id=
    fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP

. .aliases

load_repo_config

use_docker_build=false
use_artifact_http_server=false
use_artifact_nginx=false
open_artifact_frontend=false
pull_current_branch=false
install_after_pull=false
install_systemd_after_install=false
systemd_command_given=false
systemd_action=
retry_failed_builds_requested=false
stay_on_current_branch=false
environment_build_command=${GITTALLY_BUILD_COMMAND-"$default_build_command"}
build_command=$(config_value GITTALLY_BUILD_COMMAND "" "$default_build_command")
build_clean_command=$(config_value GITTALLY_BUILD_CLEAN_COMMAND "" "$default_build_clean_command")
build_artefact_dirs=$(config_value GITTALLY_BUILD_ARTEFACT_DIRS "" "$default_build_artefact_dirs")
build_stdout_log=$(config_value GITTALLY_BUILD_STDOUT_LOG "" "$default_build_stdout_log")
build_stderr_log=$(config_value GITTALLY_BUILD_STDERR_LOG "" "$default_build_stderr_log")
new_branch_commit_max_age=$(config_value GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE "" "$default_new_branch_commit_max_age")
auto_build_branches=$(config_value GITTALLY_AUTO_BUILD_BRANCHES "" "")
auto_build_times=$(config_value GITTALLY_AUTO_BUILD_TIMES "" "$default_auto_build_times")
docker_build_image=$(config_value GITTALLY_BUILD_DOCKER_IMAGE HSADMIN_NG_BUILD_IMAGE "$default_build_docker_image")
docker_build_dockerfile=$(config_value GITTALLY_BUILD_DOCKERFILE "" "$default_build_dockerfile")
docker_build_context=$(config_value GITTALLY_BUILD_DOCKER_CONTEXT "" "$default_build_docker_context")
docker_build_network=$(config_value GITTALLY_BUILD_DOCKER_NETWORK HSADMIN_NG_BUILD_NETWORK "$default_build_docker_network")
docker_build_preflight_command=$(config_value GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND "" "$default_build_docker_preflight_command")
docker_build_env=$(config_value GITTALLY_BUILD_DOCKER_ENV "" "$default_build_docker_env")
docker_build_java_tool_options=$(config_value GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS "" "$default_build_docker_java_tool_options")
bootstrap_docker_build_image="$docker_build_image"
bootstrap_docker_build_dockerfile="$docker_build_dockerfile"
bootstrap_docker_build_context="$docker_build_context"
bootstrap_docker_build_network="$docker_build_network"
bootstrap_docker_build_preflight_command="$docker_build_preflight_command"
bootstrap_docker_build_env="$docker_build_env"
bootstrap_docker_build_java_tool_options="$docker_build_java_tool_options"
artifact_http_server_port=$(config_value GITTALLY_ARTIFACT_SERVER_PORT HSADMIN_NG_ARTIFACT_SERVER_PORT 18080)
artifact_http_server_bind_address=$(config_value GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS HSADMIN_NG_ARTIFACT_SERVER_BIND_ADDRESS 0.0.0.0)
artifact_http_server_host=$(config_value GITTALLY_ARTIFACT_SERVER_HOST HSADMIN_NG_ARTIFACT_SERVER_HOST "")
artifact_public_base_url=$(config_value GITTALLY_ARTIFACT_PUBLIC_BASE_URL HSADMIN_NG_ARTIFACT_PUBLIC_BASE_URL "")
artifact_build_retention_per_branch=$(config_value GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH HSADMIN_NG_ARTIFACT_BUILD_RETENTION_PER_BRANCH 3)
artifact_nginx_server_name=$(config_value GITTALLY_ARTIFACT_NGINX_SERVER_NAME HSADMIN_NG_ARTIFACT_NGINX_SERVER_NAME "")
artifact_nginx_http_port=$(config_value GITTALLY_ARTIFACT_NGINX_HTTP_PORT HSADMIN_NG_ARTIFACT_NGINX_HTTP_PORT 8080)
artifact_nginx_https_port=$(config_value GITTALLY_ARTIFACT_NGINX_HTTPS_PORT HSADMIN_NG_ARTIFACT_NGINX_HTTPS_PORT 8443)
artifact_nginx_upstream_host=$(config_value GITTALLY_ARTIFACT_NGINX_UPSTREAM_HOST HSADMIN_NG_ARTIFACT_NGINX_UPSTREAM_HOST "")
artifact_nginx_container_name=$(config_value GITTALLY_ARTIFACT_NGINX_CONTAINER_NAME HSADMIN_NG_ARTIFACT_NGINX_CONTAINER_NAME "")
artifact_nginx_state_dir=$(config_value GITTALLY_ARTIFACT_NGINX_STATE_DIR HSADMIN_NG_ARTIFACT_NGINX_STATE_DIR "")
artifact_letsencrypt_email=$(config_value GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL HSADMIN_NG_ARTIFACT_LETSENCRYPT_EMAIL "")
artifact_certbot_env=$(config_value GITTALLY_ARTIFACT_CERTBOT_ENV HSADMIN_NG_ARTIFACT_CERTBOT_ENV "")
artifact_auth_mode=$(config_value GITTALLY_ARTIFACT_AUTH_MODE "" "")
artifact_auth_gitea_base_url=$(config_value GITTALLY_ARTIFACT_AUTH_GITEA_BASE_URL "" "")
artifact_auth_client_id=$(config_value GITTALLY_ARTIFACT_AUTH_CLIENT_ID "" "")
artifact_auth_client_secret=$(config_value GITTALLY_ARTIFACT_AUTH_CLIENT_SECRET "" "")
artifact_auth_cookie_secret=$(config_value GITTALLY_ARTIFACT_AUTH_COOKIE_SECRET "" "")
artifact_auth_cookie_domain=$(config_value GITTALLY_ARTIFACT_AUTH_COOKIE_DOMAIN "" "")
artifact_auth_image=$(config_value GITTALLY_ARTIFACT_AUTH_IMAGE "" "$default_artifact_auth_image")
artifact_auth_container_name=$(config_value GITTALLY_ARTIFACT_AUTH_CONTAINER_NAME "" "")
artifact_auth_http_port=$(config_value GITTALLY_ARTIFACT_AUTH_HTTP_PORT "" "$default_artifact_auth_http_port")
artifact_auth_email_domains=$(config_value GITTALLY_ARTIFACT_AUTH_EMAIL_DOMAINS "" "$default_artifact_auth_email_domains")
impressum_url=$(config_value GITTALLY_IMPRESSUM_URL "" "$default_impressum_url")
artifact_http_server_pid=
artifact_http_server_local_url=
artifact_http_server_url=
gitea_base_url=$(config_value GITTALLY_GITEA_BASE_URL HSADMIN_NG_GITEA_BASE_URL "")
gitea_owner=$(config_value GITTALLY_GITEA_OWNER HSADMIN_NG_GITEA_OWNER "")
gitea_repo=$(config_value GITTALLY_GITEA_REPO HSADMIN_NG_GITEA_REPO "")
gitea_git_username=$(config_value GITTALLY_GITEA_GIT_USERNAME HSADMIN_NG_GITEA_GIT_USERNAME "")
gitea_token=$(config_value GITTALLY_GITEA_TOKEN HSADMIN_NG_GITEA_TOKEN "")
gitea_status_context=$(config_value GITTALLY_GITEA_STATUS_CONTEXT HSADMIN_NG_GITEA_STATUS_CONTEXT "$default_gitea_status_context")
gitea_status_target_url=$(config_value GITTALLY_GITEA_STATUS_TARGET_URL HSADMIN_NG_GITEA_STATUS_TARGET_URL "")
branches_to_build=()

usage() {
    echo "$tool_name - a simple Gitea branch CI tally."
    echo "Checks for branches on origin, pulls, builds, and records their states, logs, reports, and artifacts."
    echo "Usage: $0 [--install] [--systemd] [--env] [--pull] [--docker] [--http] [--nginx] [--retry] [--stay] [--open] [branch ...]"
    echo
    echo "With --install, this script and systemd config files are installed to GITTALLY_INSTALL_DIR,"
    echo "    defaulting to the current user's ~/bin directory."
    echo "    It also installs gitTally-update to pull, reinstall, restart, and watch the service."
    echo "With --systemd, --install also installs/reloads the generated systemd user service."
    echo "Systemd actions: --systemd:start, --systemd:stop, --systemd:reload, --systemd:status,"
    echo "    --systemd:log, --systemd:watch, --systemd:enable, --systemd:disable."
    echo "With --env, supported environment variables with detected defaults are printed."
    echo "With --pull, the current branch is pulled from origin using the configured Gitea token."
    echo "If a .gitTally file exists in the repository root, its non-secret config values are loaded."
    echo "Shell environment variables override values from .gitTally."
    echo "When combined, --pull runs before --install."
    echo "Existing local branches and recent new origin branches are watched."
    echo "New origin branches are watched if they do not exist locally yet"
    echo "    and whose latest origin commit is not older than GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE."
    echo "With --docker, the build command runs in the configured Docker image."
    echo "With --http, an HTTP server for archived build artifacts is started;"
    echo "    failed builds continue without asking to open the artifact index."
    echo "With --nginx, --http is implied and a Docker nginx reverse proxy with Let's Encrypt is configured and started."
    echo "With --open, --http is implied and the frontend is opened in the local browser once the server is running."
    echo "With --retry, branches whose latest build failed are built again even without new commits."
    echo "With --stay, only the currently checked-out branch is built; other branches stay pending."
    echo
    echo "Set GITTALLY_BUILD_COMMAND as fallback build command if the checked-out branch .gitTally does not define it;"
    echo "    branch is exported for shell expansion."
    echo "    Default: $default_build_command"
    echo "Set GITTALLY_BUILD_CLEAN_COMMAND to override the pre-build clean command; default: $default_build_clean_command"
    echo "Set GITTALLY_BUILD_ARTEFACT_DIRS to override report directories copied into artifacts; separate paths with ';'."
    echo "Set GITTALLY_BUILD_STDOUT_LOG and GITTALLY_BUILD_STDERR_LOG to override persisted build log names."
    echo "Set GITTALLY_BUILD_DOCKER_IMAGE to override the Docker image name."
    echo "Set GITTALLY_BUILD_DOCKERFILE and GITTALLY_BUILD_DOCKER_CONTEXT to override image build inputs."
    echo "Set GITTALLY_BUILD_DOCKER_NETWORK to override the Docker network mode; default: host."
    echo "Set GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND to override the container Docker access check."
    echo "Set GITTALLY_BUILD_DOCKER_ENV for additional Docker build-container env assignments, separated by spaces."
    echo "Set GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS to override Testcontainers Java defaults."
    echo "Set GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE to override how long new origin branches are considered recent;"
    echo "    use a value ending in h/d; default: $default_new_branch_commit_max_age."
    echo "Legacy HSADMIN_NG_BUILD_IMAGE and HSADMIN_NG_BUILD_NETWORK are still accepted as fallbacks."
    echo
    echo "Set GITTALLY_ARTIFACT_SERVER_PORT to override the preferred artifact server port; default: 18080."
    echo "Set GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS to override the artifact server bind address; default: 0.0.0.0."
    echo "Set GITTALLY_ARTIFACT_SERVER_HOST to override the host printed in artifact server URLs."
    echo "Set GITTALLY_ARTIFACT_PUBLIC_BASE_URL to override public artifact URLs, for example behind nginx."
    echo "Set GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH to override retained builds per branch;"
    echo "    use a number for count, or a value ending in h/d for age; default: 3."
    echo "Set GITTALLY_ARTIFACT_NGINX_SERVER_NAME to configure the nginx/Let's Encrypt hostname."
    echo "Set GITTALLY_ARTIFACT_NGINX_HTTP_PORT and GITTALLY_ARTIFACT_NGINX_HTTPS_PORT to override nginx host ports; defaults: 8080/8443."
    echo "Set GITTALLY_ARTIFACT_NGINX_UPSTREAM_HOST to override the host nginx uses for the artifact HTTP server."
    echo "Set GITTALLY_ARTIFACT_NGINX_CONTAINER_NAME to override the Docker container name."
    echo "Set GITTALLY_ARTIFACT_NGINX_STATE_DIR to override the persistent nginx/certbot state directory."
    echo "Set GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL to register the certificate with an email address."
    echo "Set GITTALLY_ARTIFACT_CERTBOT_ENV for extra certbot args, for example --staging."
    echo "Set GITTALLY_ARTIFACT_AUTH_MODE=gitea-oauth2 to require Gitea browser login for the nginx artifact website."
    echo "Set GITTALLY_ARTIFACT_AUTH_GITEA_BASE_URL to choose the Gitea login base URL; default: detected Gitea URL or https://dev.hostsharing.net."
    echo "Set GITTALLY_ARTIFACT_AUTH_CLIENT_ID, GITTALLY_ARTIFACT_AUTH_CLIENT_SECRET, and GITTALLY_ARTIFACT_AUTH_COOKIE_SECRET for OAuth2 login."
    echo "Set GITTALLY_ARTIFACT_AUTH_EMAIL_DOMAINS to restrict logged-in Gitea users by email domain; default: *."
    echo "Legacy HSADMIN_NG_ARTIFACT_* variables are still accepted as fallbacks."
    echo
    echo "Set GITTALLY_GITEA_TOKEN to publish/read build statuses and authenticate HTTPS git commands via Gitea; required for startup."
    echo "Set GITTALLY_GITEA_GIT_USERNAME to authenticate HTTPS git commands with GITTALLY_GITEA_TOKEN; required for startup."
    echo "Set GITTALLY_GITEA_BASE_URL, GITTALLY_GITEA_OWNER, and GITTALLY_GITEA_REPO to override origin-based detection."
    echo "Set GITTALLY_GITEA_STATUS_CONTEXT to override the status context; default: GitTally."
    echo "Set GITTALLY_GITEA_STATUS_TARGET_URL to override the status target URL."
    echo "Legacy HSADMIN_NG_GITEA_* variables are still accepted as fallbacks."
    echo
    echo "Branch arguments may be full names or unique name parts matching local or origin branches."
    echo "If multiple branches match, the script asks which one to use."
}

normalize_branch_name() {
    local branch="$1"

    branch="${branch#"${branch%%[![:space:]]*}"}"
    branch="${branch%"${branch##*[![:space:]]}"}"
    branch="${branch#\* }"
    branch="${branch#remotes/}"
    branch="${branch#remote/}"
    branch="${branch#origin/}"

    echo "$branch"
}

branch_candidates() {
    {
        git for-each-ref --format='%(refname:strip=2)' refs/heads
        git for-each-ref --format='%(refname:strip=3)' refs/remotes/origin | grep -v '^HEAD$'
    } | awk '!seen[$0]++'
}

resolve_branch_name() {
    local branch_part="$1"
    local choice
    local index
    local selected
    local matches=()

    mapfile -t matches < <(branch_candidates | grep -F -- "$branch_part")

    if [ "${#matches[@]}" -eq 0 ]; then
        echo "ERROR: no local or origin branch matches '$branch_part'." >&2
        return 1
    fi

    if [ "${#matches[@]}" -eq 1 ]; then
        echo "${matches[0]}"
        return 0
    fi

    echo "Multiple branches match '$branch_part':" >&2
    index=1
    for selected in "${matches[@]}"; do
        echo "  $index) $selected" >&2
        index=$((index + 1))
    done

    while true; do
        echo -n "Select branch [1-${#matches[@]}]: " >&2
        if ! read -r choice; then
            echo "ERROR: no branch selected." >&2
            return 1
        fi
        if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#matches[@]}" ]; then
            echo "${matches[$((choice - 1))]}"
            return 0
        fi
        echo "ERROR: invalid selection: $choice" >&2
    done
}

switch_to_branch() {
    local branch="$1"

    echo "checking out branch: $branch"
    if git show-ref --quiet --verify "refs/heads/$branch"; then
        echo "Branch $branch already exists. Checking it out."
        git switch "$branch" || return 1
    else
        echo "Creating and checking out new branch: $branch"
        git switch --track -c "$branch" "refs/remotes/origin/$branch" || return 1
    fi
}

pull_branch_if_possible() {
    local branch="$1"

    if git show-ref --quiet --verify "refs/remotes/origin/$branch"; then
        git_with_gitea_token fetch origin "$branch" || return 1
        git reset --hard "origin/$branch" || return 1
    fi
}

pull_current_branch_from_origin() {
    local branch

    branch=$(git branch --show-current)
    if [ -z "$branch" ]; then
        echo "ERROR: --pull requires a checked out branch, but HEAD is detached." >&2
        return 1
    fi

    echo "pulling current branch from origin: $branch"
    git_with_gitea_token fetch origin "$branch" || return 1
    git reset --hard "origin/$branch"
}

branch_exists_on_origin() {
    local branch="$1"

    git show-ref --quiet --verify "refs/remotes/origin/$branch"
}

branch_matches_current_worktree_branch() {
    local branch="$1"
    local current_branch

    if [ "$stay_on_current_branch" != true ]; then
        return 0
    fi

    current_branch=$(git branch --show-current) || return 1
    [ -n "$current_branch" ] && [ "$branch" = "$current_branch" ]
}

validate_stay_on_current_branch() {
    local current_branch

    if [ "$stay_on_current_branch" != true ]; then
        return 0
    fi

    current_branch=$(git branch --show-current) || return 1
    if [ -z "$current_branch" ]; then
        echo "ERROR: --stay requires a checked out branch, but HEAD is detached." >&2
        return 1
    fi

    echo "staying on current branch: $current_branch"
}

checkout_and_build() {
    local branch="$1"

    if ! branch_exists_on_origin "$branch"; then
        echo "Branch $branch no longer exists on origin. Skipping build."
        return 0
    fi

    if branch_matches_current_worktree_branch "$branch"; then
        if [ "$stay_on_current_branch" = true ]; then
            echo "staying on current branch: $branch"
        else
            switch_to_branch "$branch" || return 1
        fi
    else
        echo "Branch $branch is pending, but --stay keeps this worktree on the current branch."
        return 0
    fi
    pull_branch_if_possible "$branch" || return 1

    build_current_checkout
}

branch_has_new_commits() {
    local branch="$1"
    local upstream

    if ! git show-ref --quiet --verify "refs/heads/$branch"; then
        git show-ref --quiet --verify "refs/remotes/origin/$branch"
        return
    fi

    upstream=$(git for-each-ref --format='%(upstream)' "refs/heads/$branch") || return 2
    if [ -n "$upstream" ]; then
        has_new_commits "refs/heads/$branch" "$upstream"
    elif git show-ref --quiet --verify "refs/remotes/origin/$branch"; then
        has_new_commits "refs/heads/$branch" "refs/remotes/origin/$branch"
    else
        return 1
    fi
}

checkout_requested_branch() {
    local branch_arg="$1"
    local branch
    local branch_has_new_commits_status
    local has_remote_updates=false

    branch=$(resolve_branch_name "$branch_arg") || return 1
    if ! branch_exists_on_origin "$branch"; then
        echo "Branch $branch no longer exists on origin. Skipping build."
        return 0
    fi

    while true; do
        if branch_has_new_commits "$branch"; then
            has_remote_updates=true
            break
        fi
        branch_has_new_commits_status=$?
        if [ "$branch_has_new_commits_status" -eq 1 ]; then
            break
        fi
        echo "checking branch $branch for new commits failed; retrying in 10s ..." >&2
        sleep 10
        retry_fetch_origin
    done

    if branch_matches_current_worktree_branch "$branch"; then
        if [ "$stay_on_current_branch" = true ]; then
            echo "staying on current branch: $branch"
        else
            switch_to_branch "$branch" || return 1
        fi
    else
        echo "Branch $branch is pending, but --stay keeps this worktree on the current branch."
        return 0
    fi

    if [ "$has_remote_updates" = true ]; then
        pull_branch_if_possible "$branch" || return 1
    fi

    if [ "$has_remote_updates" = true ]; then
        build_current_checkout
    elif branch_has_restartable_build "$branch"; then
        echo "Restarting pending, interrupted, or stale running build: $branch"
        build_current_checkout
    elif [ "$retry_failed_builds_requested" = true ] && branch_has_failed_build "$branch"; then
        echo "Retrying failed build: $branch"
        build_current_checkout
    else
        echo "Branch $branch has no new commits. Skipping initial build."
    fi
}

print_build_banner() {
    local title="$1"

    echo
    printf '%*s\n' 80 '' | tr ' ' '='
    echo "$title"
    printf '%*s\n' 80 '' | tr ' ' '-'
}

open_in_local_browser() {
    local label="$1"
    local target="$2"

    if command -v xdg-open >/dev/null 2>&1; then
        xdg-open "$target" >/dev/null 2>&1 &
    elif command -v open >/dev/null 2>&1; then
        open "$target" >/dev/null 2>&1 &
    elif command -v sensible-browser >/dev/null 2>&1; then
        sensible-browser "$target" >/dev/null 2>&1 &
    else
        echo "Cannot open $label automatically: no browser opener found."
        echo "$label: $target"
        return 0
    fi

    echo "Opened $label: $target"
}

open_artifact_index() {
    local branch="$1"
    local artifact_key="${2:-}"
    local artifact_dir
    local artifact_index
    local artifact_content_index
    local artifact_url
    local artifact_target
    local artifact_display_target

    if [ -z "$artifact_key" ]; then
        artifact_key=$(build_artifact_branch_key "$branch")
    fi

    artifact_dir=$(build_artifact_dir "$branch" "$artifact_key")
    artifact_index="$artifact_dir/index.html"
    artifact_content_index=$(build_artifact_index_content_file "$artifact_dir")
    if [ ! -f "$artifact_index" ] && [ ! -f "$artifact_content_index" ]; then
        echo "Artifact index not found: $artifact_index"
        return 0
    fi
    if [ -n "$artifact_http_server_url" ]; then
        artifact_url="${artifact_http_server_url}branches/$artifact_key/index.html"
        artifact_target="$artifact_url"
        artifact_display_target="$artifact_url"
    elif [ -f "$artifact_index" ]; then
        artifact_target="$artifact_index"
        artifact_display_target="file://$artifact_target"
    else
        artifact_target="$artifact_content_index"
        artifact_display_target="file://$artifact_target"
    fi

    open_in_local_browser "artifact index" "$artifact_display_target"
}

handle_build_failure_prompt() {
    local branch="$1"
    local artifact_key="${2:-}"
    local choice

    if [ "$use_artifact_http_server" = true ]; then
        return 0
    fi

    while true; do
        echo
        printf "Build failed on branch: %s\n[o]pen artifact index, [ENTER/c]ontinue, e[x]it: " "$branch"
        if ! IFS= read -r -n 1 choice; then
            echo
            return 0
        fi
        echo

        case "$choice" in
            o|O)
                open_artifact_index "$branch" "$artifact_key"
                return 0
                ;;
            ""|c|C)
                return 0
                ;;
            x|X|q|Q)
                echo "Exiting."
                exit 1
                ;;
            *)
                echo "Please press o, c, or x."
                ;;
        esac
    done
}

build_results_file() {
    git rev-parse --git-path git-watch-origin-and-test/build-results.tsv
}

auto_builds_state_file() {
    git rev-parse --git-path git-watch-origin-and-test/auto-builds.tsv
}

build_lock_file() {
    git rev-parse --git-path git-watch-origin-and-test/build.lock
}

build_cancel_request_file() {
    git rev-parse --git-path git-watch-origin-and-test/cancel-request 2>/dev/null
}

build_cancel_token_file() {
    git rev-parse --git-path git-watch-origin-and-test/cancel-token 2>/dev/null
}

build_cancel_accepted_file() {
    git rev-parse --git-path git-watch-origin-and-test/cancel-accepted 2>/dev/null
}

new_build_cancel_token() {
    if command -v openssl >/dev/null 2>&1; then
        openssl rand -hex 24
    elif [ -r /proc/sys/kernel/random/uuid ]; then
        sed -n '1p' /proc/sys/kernel/random/uuid
    else
        printf '%s-%s-%s' "$$" "${RANDOM:-0}" "$(date +%s)"
    fi
}

write_build_cancel_token() {
    local token_file
    local token_dir

    token_file=$(build_cancel_token_file)
    token_dir=$(dirname "$token_file")
    mkdir -p "$token_dir" || return 1
    new_build_cancel_token >"$token_file"
    chmod 600 "$token_file" 2>/dev/null || true
}

read_build_cancel_token() {
    local token_file

    token_file=$(build_cancel_token_file)
    if [ -r "$token_file" ]; then
        sed -n '1p' "$token_file"
    fi
}

clear_build_cancel_request() {
    local request_file
    local token_file
    local accepted_file

    request_file=$(build_cancel_request_file)
    token_file=$(build_cancel_token_file)
    accepted_file=$(build_cancel_accepted_file)
    if [ -n "$request_file" ]; then
        rm -f "$request_file"
    fi
    if [ -n "$token_file" ]; then
        rm -f "$token_file"
    fi
    if [ -n "$accepted_file" ]; then
        rm -f "$accepted_file"
    fi
}

build_cancel_requested() {
    [ -f "$(build_cancel_request_file)" ]
}

process_is_zombie() {
    local pid="$1"
    local state

    state=$(ps -p "$pid" -o stat= 2>/dev/null || true)
    [[ "$state" == Z* ]]
}

monitor_build_cancel_request() {
    local pid="$1"
    local accepted_file="$2"

    while kill -0 "$pid" >/dev/null 2>&1; do
        if build_cancel_requested; then
            if process_is_zombie "$pid"; then
                return 0
            fi
            echo cancel >"$accepted_file"
            echo "build cancellation requested"
            terminate_process_tree "$pid" TERM
            sleep 2
            if kill -0 "$pid" >/dev/null 2>&1; then
                terminate_process_tree "$pid" KILL
            fi
            return 130
        fi
        sleep 1
    done
}

wait_for_active_build() {
    local pid="$1"
    local monitor_pid=
    local accepted_file
    local exit_code

    active_build_cancelled=false
    accepted_file=$(build_cancel_accepted_file)
    if [ -n "$accepted_file" ]; then
        rm -f "$accepted_file"
        monitor_build_cancel_request "$pid" "$accepted_file" &
        monitor_pid=$!
    fi

    wait "$pid"
    exit_code=$?
    if [ -n "$monitor_pid" ]; then
        kill "$monitor_pid" >/dev/null 2>&1 || true
        wait "$monitor_pid" 2>/dev/null || true
    fi
    if [ -n "$accepted_file" ] && [ -f "$accepted_file" ]; then
        active_build_cancelled=true
        return 130
    fi
    return "$exit_code"
}

process_command_line() {
    local pid="$1"

    if [ -r "/proc/$pid/cmdline" ]; then
        tr '\0' ' ' <"/proc/$pid/cmdline" | sed 's/[[:space:]]*$//'
    else
        ps -p "$pid" -o args= 2>/dev/null || true
    fi
}

process_working_directory() {
    local pid="$1"

    readlink -f "/proc/$pid/cwd" 2>/dev/null || true
}

process_is_current_shell() {
    local pid="$1"

    [ "$pid" = "$$" ] || [ "$pid" = "${BASHPID:-}" ] || [ "$pid" = "${PPID:-}" ]
}

terminate_process_tree() {
    local pid="$1"
    local signal="${2:-TERM}"
    local child_pid

    if ! kill -0 "$pid" >/dev/null 2>&1; then
        return 0
    fi
    if command -v pgrep >/dev/null 2>&1; then
        while IFS= read -r child_pid; do
            if [ -n "$child_pid" ]; then
                terminate_process_tree "$child_pid" "$signal"
            fi
        done < <(pgrep -P "$pid" 2>/dev/null || true)
    fi
    kill "-$signal" "$pid" >/dev/null 2>&1 || true
}

build_lock_holder_pids() {
    local lock_path="$1"

    if command -v fuser >/dev/null 2>&1; then
        fuser "$lock_path" 2>/dev/null | tr -cs '0-9' '\n' | sed '/^$/d' | sort -u
    elif command -v lsof >/dev/null 2>&1; then
        lsof -t -- "$lock_path" 2>/dev/null | sort -u
    fi
}

terminate_stale_build_lock_holders() {
    local lock_path="$1"
    local repo_dir
    local pid
    local process_dir
    local process_command
    local -a stale_pids=()

    repo_dir=$(realpath "$PWD")
    while IFS= read -r pid; do
        if [ -z "$pid" ] || process_is_current_shell "$pid"; then
            continue
        fi
        process_dir=$(process_working_directory "$pid")
        process_command=$(process_command_line "$pid")
        if [ "$process_dir" = "$repo_dir" ]; then
            echo "Terminating stale build lock holder: pid $pid ($process_command)"
            stale_pids+=("$pid")
        else
            echo "Build lock is held by pid $pid outside this repository: $process_command" >&2
        fi
    done < <(build_lock_holder_pids "$lock_path")

    if [ "${#stale_pids[@]}" -eq 0 ]; then
        return 1
    fi

    for pid in "${stale_pids[@]}"; do
        terminate_process_tree "$pid" TERM
    done
    sleep 2
    for pid in "${stale_pids[@]}"; do
        if kill -0 "$pid" >/dev/null 2>&1; then
            echo "Force killing stale build lock holder: pid $pid"
            terminate_process_tree "$pid" KILL
        fi
    done
}

build_artifacts_root() {
    echo "${TMPDIR:-/tmp}/git-watch-origin-and-test/$(repository_key)"
}

repository_simple_name() {
    basename "${repo_root:-$(git rev-parse --show-toplevel)}"
}

repository_key() {
    local current_repo_root

    current_repo_root="${repo_root:-$(git rev-parse --show-toplevel)}"
    printf '%s' "$current_repo_root" | sed 's#[^[:alnum:]._-]#_#g'
}

build_artifact_branch_key() {
    local branch="$1"
    local branch_key
    local branch_hash

    branch_key=$(printf '%s' "$branch" | sed 's#[^[:alnum:]._-]#_#g')
    branch_hash=$(printf '%s' "$branch" | sha256sum | awk '{print substr($1, 1, 12)}')

    echo "$branch_key-$branch_hash"
}

build_artifact_key() {
    local branch="$1"
    local timestamp="$2"
    local timestamp_key
    local build_hash

    timestamp_key=$(printf '%s' "$timestamp" | sed 's#[^[:alnum:]._-]#_#g')
    build_hash=$(printf '%s\t%s' "$branch" "$timestamp" | sha256sum | awk '{print substr($1, 1, 12)}')

    echo "$(build_artifact_branch_key "$branch")-$timestamp_key-$build_hash"
}

build_artifact_dir() {
    local branch="$1"
    local artifact_key="${2:-}"

    if [ -z "$artifact_key" ]; then
        artifact_key=$(build_artifact_branch_key "$branch")
    fi

    echo "$(build_artifacts_root)/branches/$artifact_key"
}

build_artifact_index_content_file() {
    local artifact_dir="$1"

    echo "$artifact_dir/artifact-index-content.html"
}

format_build_duration() {
    local duration_seconds="$1"

    printf '%02d:%02d' "$((duration_seconds / 60))" "$((duration_seconds % 60))"
}

normalize_build_result_fields() {
    if [ -z "$artifact_key" ] && [ -n "$duration" ] && ! [[ "$duration" =~ ^[0-9]+:[0-5][0-9]$ ]]; then
        artifact_key="$duration"
        duration=
    fi

    if [ -n "$duration" ] && ! [[ "$duration" =~ ^[0-9]+:[0-5][0-9]$ ]]; then
        duration=
    fi

    case "$status" in
        passed)
            status=success
            ;;
    esac

    if [ -z "$artifact_key" ]; then
        artifact_key=$(build_artifact_branch_key "$branch")
    fi
}

display_build_timestamp() {
    local timestamp="$1"

    echo "${timestamp/T/ }"
}

commit_timestamp() {
    local commit="$1"

    if [[ "$commit" =~ ^[0-9a-fA-F]{7,40}$ ]] && git cat-file -e "$commit^{commit}" 2>/dev/null; then
        git show -s --format=%cI "$commit" 2>/dev/null || true
    fi
}

normalize_base_url() {
    local base_url="$1"

    if [ -z "$base_url" ]; then
        return 0
    fi
    if [[ "$base_url" == */ ]]; then
        echo "$base_url"
    else
        echo "$base_url/"
    fi
}

base_url_host() {
    local base_url="$1"

    if [[ "$base_url" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
        printf '%s' "$base_url" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/@:]+@)?([^/:]+).*$#\2#'
    fi
}

safe_container_name_part() {
    printf '%s' "$1" | sed 's#[^[:alnum:]_.-]#-#g'
}

gittally_docker_label_args() {
    local role="$1"

    printf '%s\n' \
        --label "org.hostsharing.gittally=true" \
        --label "org.hostsharing.gittally.repository=$(repository_key)" \
        --label "org.hostsharing.gittally.role=$role"
}

artifact_nginx_default_state_dir() {
    echo "${XDG_STATE_HOME:-$HOME/.local/state}/gittally/nginx/$(repository_key)"
}

artifact_auth_enabled() {
    [ "$artifact_auth_mode" = gitea-oauth2 ]
}

validate_artifact_auth_mode() {
    case "$artifact_auth_mode" in
        ""|none)
            artifact_auth_mode=
            ;;
        gitea|gitea-oauth2)
            artifact_auth_mode=gitea-oauth2
            ;;
        *)
            echo "WARNING: invalid GITTALLY_ARTIFACT_AUTH_MODE: $artifact_auth_mode; artifact website auth disabled." >&2
            artifact_auth_mode=
            ;;
    esac
}

configure_artifact_nginx_defaults() {
    local public_base_url_host

    if [ "$use_artifact_nginx" != true ]; then
        return 0
    fi

    use_artifact_http_server=true

    if [ -z "$artifact_nginx_server_name" ]; then
        public_base_url_host=$(base_url_host "$artifact_public_base_url")
        if [ -n "$public_base_url_host" ]; then
            artifact_nginx_server_name="$public_base_url_host"
        elif [ -n "$artifact_http_server_host" ]; then
            artifact_nginx_server_name="$artifact_http_server_host"
        fi
    fi

    if [ -z "$artifact_public_base_url" ] && [ -n "$artifact_nginx_server_name" ]; then
        artifact_public_base_url="https://$artifact_nginx_server_name/"
    fi

    if [ "$artifact_http_server_port" = "$artifact_nginx_http_port" ] ||
        [ "$artifact_http_server_port" = "$artifact_nginx_https_port" ]; then
        echo "WARNING: moving artifact HTTP server from port $artifact_http_server_port to 18080 because nginx uses port $artifact_http_server_port." >&2
        artifact_http_server_port=18080
        if [ "$artifact_http_server_port" = "$artifact_nginx_http_port" ] ||
            [ "$artifact_http_server_port" = "$artifact_nginx_https_port" ]; then
            artifact_http_server_port=18081
        fi
    fi

    if [ -z "$artifact_nginx_upstream_host" ]; then
        artifact_nginx_upstream_host="$artifact_nginx_server_name"
    fi

    if [ -z "$artifact_nginx_container_name" ]; then
        artifact_nginx_container_name="gittally-nginx-$(safe_container_name_part "$(repository_simple_name)")"
    fi

    if [ -z "$artifact_nginx_state_dir" ]; then
        artifact_nginx_state_dir=$(artifact_nginx_default_state_dir)
    fi

    validate_artifact_auth_mode

    if [ -z "$artifact_auth_gitea_base_url" ]; then
        artifact_auth_gitea_base_url="${gitea_base_url:-$default_artifact_auth_gitea_base_url}"
    fi

    if [ -z "$artifact_auth_container_name" ]; then
        artifact_auth_container_name="gittally-auth-$(safe_container_name_part "$(repository_simple_name)")"
    fi
}

validate_artifact_build_retention_per_branch() {
    if ! [[ "$artifact_build_retention_per_branch" =~ ^[1-9][0-9]*([dh])?$ ]]; then
        echo "WARNING: invalid GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH: $artifact_build_retention_per_branch; using 3." >&2
        artifact_build_retention_per_branch=3
    fi
}

validate_new_branch_commit_max_age() {
    if ! [[ "$new_branch_commit_max_age" =~ ^[1-9][0-9]*[dh]$ ]]; then
        echo "WARNING: invalid GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE: $new_branch_commit_max_age; using $default_new_branch_commit_max_age." >&2
        new_branch_commit_max_age="$default_new_branch_commit_max_age"
    fi
}

validate_auto_build_times() {
    [ -n "$auto_build_branches" ] || return 0
    local valid_times="" IFS=';' slot
    for slot in $auto_build_times; do
        if [[ "$slot" =~ ^[0-2][0-9]:[0-5][0-9]$ ]]; then
            valid_times="${valid_times:+$valid_times;}$slot"
        else
            echo "WARNING: invalid time in GITTALLY_AUTO_BUILD_TIMES: '$slot'; expected HH:MM (semicolon-separated)." >&2
        fi
    done
    if [ -z "$valid_times" ] && [ -n "$auto_build_times" ]; then
        echo "WARNING: no valid times in GITTALLY_AUTO_BUILD_TIMES; auto builds disabled." >&2
    fi
    auto_build_times="$valid_times"
}

artifact_build_retention_is_count() {
    [[ "$artifact_build_retention_per_branch" =~ ^[1-9][0-9]*$ ]]
}

artifact_build_retention_cutoff_epoch() {
    local amount

    amount="${artifact_build_retention_per_branch%[dh]}"
    case "$artifact_build_retention_per_branch" in
        *h)
            echo "$(($(date +%s) - amount * 3600))"
            ;;
        *d)
            echo "$(($(date +%s) - amount * 86400))"
            ;;
    esac
}

new_branch_commit_max_age_cutoff_epoch() {
    local amount

    amount="${new_branch_commit_max_age%[dh]}"
    case "$new_branch_commit_max_age" in
        *h)
            echo "$(($(date +%s) - amount * 3600))"
            ;;
        *d)
            echo "$(($(date +%s) - amount * 86400))"
            ;;
    esac
}

detect_gitea_repo() {
    detect_gitea_repo_from_origin_url
}

gitea_status_enabled() {
    [ -n "$gitea_token" ] &&
        [ -n "$gitea_base_url" ] &&
        [ -n "$gitea_owner" ] &&
        [ -n "$gitea_repo" ] &&
        command -v curl >/dev/null 2>&1 &&
        command -v python3 >/dev/null 2>&1
}

validate_gitea_git_credentials() {
    if [ -n "$gitea_git_username" ] && [ -n "$gitea_token" ]; then
        return 0
    fi

    if [ -z "$gitea_git_username" ]; then
        echo "ERROR: GITTALLY_GITEA_GIT_USERNAME must be set before starting $tool_name." >&2
    fi
    if [ -z "$gitea_token" ]; then
        echo "ERROR: GITTALLY_GITEA_TOKEN must be set before starting $tool_name." >&2
    fi
    echo "Git commands cannot run without both Gitea credentials." >&2
    return 1
}

json_string() {
    python3 -c 'import json, sys; print(json.dumps(sys.argv[1]))' "$1"
}

origin_uses_https() {
    local origin_url

    origin_url=$(origin_url)
    [[ "$origin_url" =~ ^https?:// ]]
}

origin_url_username() {
    detect_git_username_from_origin_url
}

gitea_token_username() {
    local user_json

    gitea_status_enabled || return 1

    user_json=$(curl -fsS \
        -H "Authorization: token $gitea_token" \
        "${gitea_base_url%/}/api/v1/user" 2>/dev/null) || return 1

    printf '%s' "$user_json" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("login", ""))' 2>/dev/null
}

ensure_git_askpass_file() {
    if [ -n "$git_askpass_file" ]; then
        return 0
    fi

    git_askpass_file=$(mktemp "${TMPDIR:-/tmp}/git-watch-origin-and-test-askpass.XXXXXX") || return 1
    cat >"$git_askpass_file" <<'EOF'
#!/bin/sh
case "$1" in
    *Username*|*username*)
        printf '%s\n' "$GITTALLY_GITEA_GIT_USERNAME"
        ;;
    *)
        printf '%s\n' "$GITTALLY_GITEA_TOKEN"
        ;;
esac
EOF
    chmod 700 "$git_askpass_file"
}

git_with_gitea_token() {
    local git_username

    if [ -z "$gitea_token" ] || ! origin_uses_https; then
        git "$@"
        return
    fi

    ensure_git_askpass_file || return 1
    git_username="${gitea_git_username:-}"
    if [ -z "$git_username" ]; then
        git_username=$(origin_url_username)
    fi
    if [ -z "$git_username" ]; then
        git_username=$(gitea_token_username || true)
    fi
    if [ -z "$git_username" ]; then
        echo "WARNING: GITTALLY_GITEA_TOKEN is set, but no HTTPS git username could be determined." >&2
        echo "Set GITTALLY_GITEA_GIT_USERNAME to use the token for git fetch/pull." >&2
        git "$@"
        return
    fi

    GITTALLY_GITEA_GIT_USERNAME="$git_username" \
        GITTALLY_GITEA_TOKEN="$gitea_token" \
        GIT_ASKPASS="$git_askpass_file" \
        GIT_TERMINAL_PROMPT=0 \
        git "$@"
}

gitea_status_state_for_build_status() {
    case "$1" in
        success|passed)
            echo success
            ;;
        failed|interrupted|cancelled)
            echo failure
            ;;
        pending|running)
            echo pending
            ;;
        *)
            echo error
            ;;
    esac
}

gitea_deleted_status_description() {
    echo "Build status deleted"
}

build_status_for_gitea_status_state() {
    case "$1" in
        success)
            echo success
            ;;
        failure|error|warning)
            echo failed
            ;;
        pending)
            echo running
            ;;
        *)
            return 1
            ;;
    esac
}

gitea_status_api_url() {
    local sha="$1"

    printf '%s/api/v1/repos/%s/%s/statuses/%s' \
        "${gitea_base_url%/}" \
        "$gitea_owner" \
        "$gitea_repo" \
        "$sha"
}

gitea_commit_status_api_url() {
    local sha="$1"

    printf '%s/api/v1/repos/%s/%s/commits/%s/statuses?sort=recentupdate' \
        "${gitea_base_url%/}" \
        "$gitea_owner" \
        "$gitea_repo" \
        "$sha"
}

gitea_status_target_url_for_branch() {
    local branch="$1"
    local artifact_key="${2:-}"

    if [ -n "$gitea_status_target_url" ]; then
        echo "$gitea_status_target_url"
    elif [ -n "$artifact_http_server_url" ]; then
        if [ -z "$artifact_key" ]; then
            artifact_key=$(build_artifact_branch_key "$branch")
        fi
        echo "${artifact_http_server_url}branches/$artifact_key/index.html"
    fi
}

publish_gitea_build_status() {
    local sha="$1"
    local build_status="$2"
    local branch="$3"
    local artifact_key="${4:-}"
    local state
    local description
    local target_url
    local payload
    local status_url

    gitea_status_enabled || return 0

    state=$(gitea_status_state_for_build_status "$build_status")
    target_url=$(gitea_status_target_url_for_branch "$branch" "$artifact_key")
    case "$build_status" in
        success|passed)
            description="Build succeeded"
            ;;
        failed)
            description="Build failed"
            ;;
        cancelled)
            description="Build cancelled"
            ;;
        interrupted)
            description="Build interrupted"
            ;;
        pending|running)
            description="Build running"
            ;;
        *)
            description="Build status unknown"
            ;;
    esac

    payload=$(
        printf '{"state":%s,"context":%s,"description":%s' \
            "$(json_string "$state")" \
            "$(json_string "$gitea_status_context")" \
            "$(json_string "$description")"
        if [ -n "$target_url" ]; then
            printf ',"target_url":%s' "$(json_string "$target_url")"
        fi
        printf '}'
    )
    status_url=$(gitea_status_api_url "$sha")

    if ! curl -fsS \
        -H "Authorization: token $gitea_token" \
        -H "Content-Type: application/json" \
        -X POST \
        -d "$payload" \
        "$status_url" >/dev/null; then
        echo "WARNING: could not publish Gitea build status for $sha." >&2
        return 1
    fi
}

read_gitea_build_status() {
    local sha="$1"
    local statuses_json
    local state
    local response_file
    local http_status

    gitea_status_enabled || return 1

    response_file=$(mktemp "${TMPDIR:-/tmp}/gittally-gitea-status.XXXXXX") || return 1
    http_status=$(curl -sS \
        -w '%{http_code}' \
        -o "$response_file" \
        -H "Authorization: token $gitea_token" \
        "$(gitea_commit_status_api_url "$sha")") || {
        rm -f -- "$response_file"
        return 1
    }
    statuses_json=$(cat "$response_file")
    rm -f -- "$response_file"
    if [ "$http_status" = 404 ]; then
        return 1
    fi
    if [ "$http_status" -lt 200 ] || [ "$http_status" -ge 300 ]; then
        echo "WARNING: could not read Gitea build status for $sha: HTTP $http_status." >&2
        return 1
    fi

    state=$(
        GITEA_STATUS_CONTEXT="$gitea_status_context" python3 -c '
import json
import os
import sys

context = os.environ["GITEA_STATUS_CONTEXT"]
statuses = json.load(sys.stdin)
for status in statuses:
    if status.get("context") == context:
        if status.get("description") == "Build status deleted":
            print("deleted")
        else:
            print(status.get("state", ""))
        break
' <<<"$statuses_json"
    )

    build_status_for_gitea_status_state "$state"
}

effective_build_status() {
    local commit="$1"
    local status="$2"

    case "$status" in
        pending|interrupted|cancelled)
            echo "$status"
            ;;
        *)
            if gitea_status_enabled; then
                read_gitea_build_status "$commit" || echo "$status"
            else
                echo "$status"
            fi
            ;;
    esac
}

html_escape() {
    sed \
        -e 's/&/\&amp;/g' \
        -e 's/</\&lt;/g' \
        -e 's/>/\&gt;/g' \
        -e 's/"/\&quot;/g'
}

url_path_escape() {
    local value="$1"
    local safe="${2:-}"

    if command -v python3 >/dev/null 2>&1; then
        python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=sys.argv[2]))' "$value" "$safe"
    else
        printf '%s' "$value"
    fi
}

gitea_repo_web_url() {
    if [ -n "$gitea_base_url" ] && [ -n "$gitea_owner" ] && [ -n "$gitea_repo" ]; then
        printf '%s/%s/%s' "${gitea_base_url%/}" "$gitea_owner" "$gitea_repo"
    fi
}

gitea_branch_web_url() {
    local branch="$1"
    local repo_url

    repo_url=$(gitea_repo_web_url)
    if [ -n "$repo_url" ]; then
        printf '%s/src/branch/%s' "$repo_url" "$(url_path_escape "$branch" '/')"
    fi
}

gitea_commit_web_url() {
    local commit="$1"
    local repo_url

    repo_url=$(gitea_repo_web_url)
    if [ -n "$repo_url" ]; then
        printf '%s/commit/%s' "$repo_url" "$(url_path_escape "$commit")"
    fi
}

write_html_link() {
    local index_file="$1"
    local href="$2"
    local label="$3"

    printf '        <li><a href="%s">%s</a></li>\n' \
        "$(printf '%s' "$href" | html_escape)" \
        "$(printf '%s' "$label" | html_escape)" \
        >>"$index_file"
}

html_copy_button() {
    local value="$1"
    local label="$2"

    printf '<button class="copy-button" type="button" data-copy="%s" title="Copy %s" aria-label="Copy %s">⧉</button>' \
        "$(printf '%s' "$value" | html_escape)" \
        "$(printf '%s' "$label" | html_escape)" \
        "$(printf '%s' "$label" | html_escape)"
}

write_html_favicon_links() {
    local index_file="$1"
    local href="${2:-favicon.svg}"

    {
        printf '    <link rel="icon" href="%s" type="image/svg+xml">\n' "$(printf '%s' "$href" | html_escape)"
        printf '    <link rel="shortcut icon" href="%s" type="image/svg+xml">\n' "$(printf '%s' "$href" | html_escape)"
    } >>"$index_file"
}

write_html_favicon() {
    local artifacts_root="$1"
    local icon_file="$artifacts_root/favicon.svg"

    mkdir -p "$artifacts_root" || return 1
    cat >"$icon_file" <<-'EOF'
	<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="gitTally">
	  <rect width="64" height="64" rx="14" fill="#155eef"/>
	  <path d="M17 47V18m0 14h13c7 0 10-4 10-11" fill="none" stroke="#f9fafb" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
	  <circle cx="17" cy="18" r="5" fill="#DD4901"/>
	  <circle cx="17" cy="47" r="5" fill="#DD4901"/>
	  <circle cx="40" cy="21" r="5" fill="#DD4901"/>
	  <path d="M44 35v14M51 35v14M58 35v14M43 47h16" fill="none" stroke="#f9fafb" stroke-width="4" stroke-linecap="round"/>
	</svg>
	EOF
}

write_script_download() {
    local artifacts_root="$1"
    local download_file="$artifacts_root/gitTally.sh"

    mkdir -p "$artifacts_root" || return 1
    cp "$script_path" "$download_file" || return 1
    chmod 644 "$download_file"
}

write_html_footer() {
    local index_file="$1"
    local license_href="${2:-license.html}"
    local about_href="${3:-about.html}"

    {
        printf '    <footer class="site-footer">'
        printf '<a href="%s"><strong><em>gitTally v%s</em></strong></a> <a href="env.html">(env)</a> ' \
            "$(printf '%s' "$about_href" | html_escape)" \
            "$(printf '%s' "$script_version" | html_escape)"
        printf -- '- (c) <a href="https://michael.hoennig.de" target="_blank" rel="noopener noreferrer">Michael Hönnig</a>, 2026 '
        printf -- '- Licensed under the <a href="%s">MIT License</a> ' "$(printf '%s' "$license_href" | html_escape)"
        printf -- '- <a href="%s" target="_blank" rel="noopener noreferrer">Impressum (Legal Disclosure)</a>' "$(printf '%s' "$impressum_url" | html_escape)"
        printf '</footer>\n'
    } >>"$index_file"
}

write_html_about_page() {
    local artifacts_root="$1"
    local index_file="$artifacts_root/about.html"

    mkdir -p "$artifacts_root" || return 1
    cat >"$index_file" <<-EOF
	<!doctype html>
	<html lang="en">
	<head>
	    <meta charset="utf-8">
	    <meta name="viewport" content="width=device-width, initial-scale=1">
	    <title>gitTally - About</title>
	    <link rel="icon" href="favicon.svg" type="image/svg+xml">
	    <link rel="shortcut icon" href="favicon.svg" type="image/svg+xml">
	    <style>
	        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --link: #155eef; }
	        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --link: #93c5fd; } }
	        * { box-sizing: border-box; }
	        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
	        main { width: min(1180px, calc(100% - 32px)); margin: 32px auto; }
	        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }
	        h1 img { width: 32px; height: 32px; flex: none; }
	        .title-home { display: inline-flex; flex: none; }
	        h2 { margin: 26px 0 12px; font-size: 18px; }
	        article { border: 1px solid var(--border); border-radius: 8px; background: var(--panel); padding: 22px; }
	        article p { margin: 0 0 16px; }
	        article ul { margin: 0 0 16px 20px; padding: 0; }
	        article li { margin: 0 0 7px; }
	        a { color: var(--link); font-weight: 650; text-decoration: none; }
	        a:hover { text-decoration: underline; }
	        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }
	        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }
	        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }
	        .view-toggle span { background: var(--link); color: white; }
	        .view-toggle a { color: var(--link); }
	        .view-toggle a:hover { background: color-mix(in srgb, var(--link) 8%, transparent); text-decoration: none; }
	        .site-footer { width: min(1180px, calc(100% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }
	        @media (max-width: 680px) {
	            body { font-size: 14px; }
	            main { margin: 16px auto; }
	            h1 { font-size: 22px; }
	            .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; }
	        }
	    </style>
	</head>
	<body>
	    <main>
	        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>About <em>gitTally</em></h1>
	        <div class="view-row"><nav class="view-toggle" aria-label="Build artifact view"><a href="index.html">Latest</a><a href="branches.html">Branches</a><a href="history.html">Builds</a><a href="current.html">Current</a></nav></div>
	        <article>
	            <p><em>gitTally</em> is a deliberately small and opinionated CI and deployment tool
	               for projects that do not have the hardware budget or operational staff for large CI/CD systems.</p>
	            <p>It is a <em>Hostsharing</em> community project,
	               not an official project of <a href="https://www.hostsharing.net/" target="_blank" rel="noopener noreferrer">Hostsharing eG</a>.</p>

	            <h2>Intention</h2>
	            <ul>
	                <li>Configuration is environment-driven, with no UI settings, so installations remain easy to bootstrap and repeatable.</li>
	              <li>It can be started right within any git working tree, even locally on the developers  computer or on a spare computer.</li>
                <li>Designed for <a href="https://www.hostsharing.net/container/container-server/" target="_blank" rel="noopener noreferrer">Hostsharing Container Server</a> environments with Docker or Podman.</li>
              </ul>

	            <h2>Operating Model</h2>
	            <p><em>gitTally</em> watches branches, checks out new commits, runs a configurable build command,
	              then archives build output for later inspection.
	              <a href="https://gitea.io/" target="_blank" rel="noopener noreferrer"><em>GitEA</ea></a> integration can publish commit status and protect the artifact website through <em>OAuth2</em> login.</p>
	            <ul>
	                <li>Build run directly in the environment or optionally in a <em>Docker</em> container.</li>
	                <li>The build command is configurable, so <em>gitTally</em> is build-system agnostic.</li>
	                <li>Build-status for the branches are kept locally and are pushed to a <em>GitEA</em> instance.</li>
	            </ul>

	            <h2>Runtime Environment</h2>
	            <p>The goal is low-cost operation without a dedicated VM per project.
	               <em>gitTally</em> is meant to run as a normal <em>Linux</em> user without root privileges.</p>
	            <ul>
	                <li>Can also run on a local computer for small projects or personal workflows.</li>
	                <li>Supports systemd user services for unattended operation.</li>
	                <li>Provides optional <em>nginx</em> reverse-proxy support with <em>Let's Encrypt</em> certificates.</li>
	            </ul>

	            <h2>Web Interface</h2>
	            <p>The web interface exposes the information needed to inspect current and past builds, while staying simple and static.</p>
	            <ul>
	                <li>Latest, branches, builds, and current-build views.</li>
	                <li>Archived stdout, stderr, and reports for each build.</li>
	                <li>Optional cancellation of the currently running build.</li>
	                <li>Static HTML served by the built-in artifact HTTP server or through <em>nginx</em>.</li>
	            </ul>

	            <h2>Roadmap</h2>
	            <p><em>gitTally</em> is currently a <em>bash</em> script, developed (mostly vibe-coded) with
	               <a href="https://www.jetbrains.com/help/ai-assistant/ai-chat.html" target="_blank" rel="noopener noreferrer">IntelliJ IDEA AI Chat</a>,
	               mainly powered by <a href="https://openai.com/index/introducing-codex/" target="_blank" rel="noopener noreferrer">Codex</a>
	               and <a href="https://openai.com/index/introducing-gpt-5-5/" target="_blank" rel="noopener noreferrer">GPT-5.5</a>
	               It may later get refactored to maintainable code in <em>Kotlin</em> or <em>Python</em>.</p>
	            <p>Planned features: Support for ...</p>
	            <ul>
	                <li>Separate the builder from the watcher, so that new branches can get detected during a build.</li>
	                <li><em>GitEA</em> PRs including green build as quality-gate for merging to master/main,</li>
	                <li>separate build-command for special branches like master/main.,</li>
	                <li><em>Docker</em>-based deployments for branches,</li>
	                <li>rootles <em>Podman</em> environments.</li>
	            </ul>
	            <p><a href="gitTally.sh" download="gitTally.sh">Download the current <em>gitTally</em> script</a>.</p>
	        </article>
	    </main>
	EOF
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

write_html_license_page() {
    local artifacts_root="$1"
    local index_file="$artifacts_root/license.html"

    mkdir -p "$artifacts_root" || return 1
    {
        printf '<!doctype html>\n'
        printf '<html lang="en">\n'
        printf '<head>\n'
        printf '    <meta charset="utf-8">\n'
        printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
        printf '    <title>GitTally - MIT License</title>\n'
    } >"$index_file"
    write_html_favicon_links "$index_file"
    {
        printf '    <style>\n'
        printf '        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --link: #155eef; }\n'
        printf '        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --link: #93c5fd; } }\n'
        printf '        * { box-sizing: border-box; }\n'
        printf '        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n'
        printf '        main { width: min(1180px, calc(100%% - 32px)); margin: 32px auto; }\n'
        printf '        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }\n'
        printf '        h1 img { width: 32px; height: 32px; flex: none; }\n'
        printf '        .title-home { display: inline-flex; flex: none; }\n'
        printf '        article { border: 1px solid var(--border); border-radius: 8px; background: var(--panel); padding: 22px; }\n'
        printf '        article p { margin: 0 0 16px; }\n'
        printf '        article p:last-child { margin-bottom: 0; }\n'
        printf '        a { color: var(--link); font-weight: 650; text-decoration: none; }\n'
        printf '        a:hover { text-decoration: underline; }\n'
        printf '        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }\n'
        printf '        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }\n'
        printf '        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }\n'
        printf '        .view-toggle span { background: var(--link); color: white; }\n'
        printf '        .view-toggle a { color: var(--link); }\n'
        printf '        .view-toggle a:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); text-decoration: none; }\n'
        printf '        .site-footer { width: min(1180px, calc(100%% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }\n'
        printf '        @media (max-width: 680px) { body { font-size: 14px; } main { margin: 16px auto; } h1 { font-size: 22px; } .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; } }\n'
        printf '    </style>\n'
        printf '</head>\n'
        printf '<body>\n'
        printf '    <main>\n'
        printf '        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>The MIT License</h1>\n'
        printf '        <div class="view-row"><nav class="view-toggle" aria-label="Build artifact view"><a href="index.html">Latest</a><a href="branches.html">Branches</a><a href="history.html">Builds</a><a href="current.html">Current</a></nav></div>\n'
        printf '        <article>\n'
        printf '            <p>Copyright 2026 Michael Hönnig</p>\n'
        printf '            <p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>\n'
        printf '            <p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>\n'
        printf '            <p>THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>\n'
        printf '        </article>\n'
        printf '    </main>\n'
    } >>"$index_file"
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

write_build_artifact_view_toggle() {
    local index_file="$1"
    local current_view_label="$2"
    local right_html="${3:-}"

    printf '        <div class="view-row">\n' >>"$index_file"
    printf '        <nav class="view-toggle" aria-label="Build artifact view">\n' >>"$index_file"
    if [ "$current_view_label" = "Latest" ]; then
        printf '            <span>Latest</span><a href="branches.html">Branches</a><a href="history.html">Builds</a><a href="current.html">Current</a><a href="system.html">System</a>\n' >>"$index_file"
    elif [ "$current_view_label" = "Branches" ]; then
        printf '            <a href="index.html">Latest</a><span>Branches</span><a href="history.html">Builds</a><a href="current.html">Current</a><a href="system.html">System</a>\n' >>"$index_file"
    elif [ "$current_view_label" = "Builds" ]; then
        printf '            <a href="index.html">Latest</a><a href="branches.html">Branches</a><span>Builds</span><a href="current.html">Current</a><a href="system.html">System</a>\n' >>"$index_file"
    elif [ "$current_view_label" = "Current" ]; then
        printf '            <a href="index.html">Latest</a><a href="branches.html">Branches</a><a href="history.html">Builds</a><span>Current</span><a href="system.html">System</a>\n' >>"$index_file"
    elif [ "$current_view_label" = "System" ]; then
        printf '            <a href="index.html">Latest</a><a href="branches.html">Branches</a><a href="history.html">Builds</a><a href="current.html">Current</a><span>System</span>\n' >>"$index_file"
    else
        printf '            <a href="index.html">Latest</a><a href="branches.html">Branches</a><a href="history.html">Builds</a><a href="current.html">Current</a><a href="system.html">System</a>\n' >>"$index_file"
    fi
    printf '        </nav>\n' >>"$index_file"
    if [ -n "$right_html" ]; then
        printf '        <div class="view-row-actions">%s</div>\n' "$right_html" >>"$index_file"
    fi
    printf '        </div>\n' >>"$index_file"
}

write_artifacts_root_index_page() {
    local view="$1"
    local artifacts_root
    local index_file
    local results_file
    local branch
    local commit
    local status
    local display_status
    local status_class
    local timestamp
    local commit_timestamp_value
    local display_commit_timestamp
    local display_status_timestamp
    local duration
    local artifact_key
    local commit_abbrev
    local branch_cell
    local branch_copy_button
    local branch_url
    local commit_cell
    local commit_copy_button
    local commit_url
    local page_title
    local current_view_label
    local reload_action_html
    local return_to
    local load_statuses_in_browser=false
    local row_class
    local local_status
    local loading_status=false
    local result_line
    local has_results=false
    local row_index=0

    artifacts_root=$(build_artifacts_root)
    results_file=$(build_results_file)
    if [ "$use_artifact_http_server" = true ] && gitea_status_enabled; then
        load_statuses_in_browser=true
    fi
    write_html_favicon "$artifacts_root" || return 1
    write_script_download "$artifacts_root" || return 1
    write_html_about_page "$artifacts_root" || return 1
    write_html_license_page "$artifacts_root" || return 1
    case "$view" in
        latest)
            index_file="$artifacts_root/index.html"
            page_title="GitTally [$(repository_simple_name)] - Latest Branch Builds"
            current_view_label="Latest"
            ;;
        branches)
            index_file="$artifacts_root/branches.html"
            page_title="GitTally [$(repository_simple_name)] - Branches"
            current_view_label="Branches"
            ;;
        history|*)
            index_file="$artifacts_root/history.html"
            page_title="GitTally [$(repository_simple_name)] - Builds"
            current_view_label="Builds"
            ;;
    esac
    reload_action_html=$(printf '<form class="reload-form" method="get" action="%s"><button class="reload-button" type="submit" title="Reload view" aria-label="Reload view">⟳</button></form>' "$(basename "$index_file")")

    mkdir -p "$artifacts_root" || return 1

    {
        printf '<!doctype html>\n'
        printf '<html lang="en">\n'
        printf '<head>\n'
        printf '    <meta charset="utf-8">\n'
        printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
        printf '    <meta http-equiv="Cache-Control" content="no-store">\n'
        printf '    <meta http-equiv="Pragma" content="no-cache">\n'
        printf '    <meta http-equiv="Expires" content="0">\n'
        printf '    <title>%s</title>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >"$index_file"
    write_html_favicon_links "$index_file"
    {
        printf '    <style>\n'
        printf '        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --row: #f9fafb; --link: #155eef; --success-bg: #dcfce7; --success-text: #166534; --failed-bg: #fee2e2; --failed-text: #991b1b; --running-bg: #dbeafe; --running-text: #1d4ed8; --pending-bg: #ede9fe; --pending-text: #5b21b6; --interrupted-bg: #ffedd5; --interrupted-text: #9a3412; --cancelled-bg: #e5e7eb; --cancelled-text: #374151; --unknown-bg: #f3f4f6; --unknown-text: #4b5563; }\n'
        printf '        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --row: #182235; --link: #93c5fd; --success-bg: #12351f; --success-text: #86efac; --failed-bg: #3f1717; --failed-text: #fca5a5; --running-bg: #112c55; --running-text: #93c5fd; --pending-bg: #2e1065; --pending-text: #c4b5fd; --interrupted-bg: #431f0b; --interrupted-text: #fdba74; --cancelled-bg: #374151; --cancelled-text: #d1d5db; --unknown-bg: #374151; --unknown-text: #d1d5db; } }\n'
        printf '        * { box-sizing: border-box; }\n'
        printf '        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n'
        printf '        main { width: min(1180px, calc(100%% - 32px)); margin: 32px auto; }\n'
        printf '        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }\n'
        printf '        h1 img { width: 32px; height: 32px; flex: none; }\n'
        printf '        .title-home { display: inline-flex; flex: none; }\n'
        printf '        .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 28px rgb(15 23 42 / 0.08); }\n'
        printf '        table { width: 100%%; border-collapse: collapse; min-width: 900px; }\n'
        printf '        th, td { padding: 12px 14px; text-align: left; vertical-align: middle; border-bottom: 1px solid var(--border); }\n'
        printf '        th { position: sticky; top: 0; background: var(--panel); color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: 0; text-transform: uppercase; }\n'
        printf '        .sort-button { appearance: none; display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-weight: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }\n'
        printf '        .sort-button:hover { color: var(--link); }\n'
        printf '        .sort-button::after { content: "↕"; color: var(--muted); font-size: 10px; }\n'
        printf '        .sort-button.is-active[data-sort-direction="asc"]::after { content: "↑"; color: var(--link); }\n'
        printf '        .sort-button.is-active[data-sort-direction="desc"]::after { content: "↓"; color: var(--link); }\n'
        printf '        tbody tr:nth-child(even) { background: var(--row); }\n'
        printf '        tbody tr:last-child td { border-bottom: 0; }\n'
        printf '        tbody tr:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); }\n'
        printf '        tbody { opacity: 1; transition: opacity 140ms ease-in-out; }\n'
        printf '        tbody.is-refreshing { opacity: 0.35; }\n'
        printf '        tbody tr.status-loading td:not(.actions-cell) { position: relative; color: transparent; }\n'
        printf '        tbody tr.status-loading td:not(.actions-cell) > * { visibility: hidden; }\n'
        printf '        tbody tr.status-loading td:not(.actions-cell)::after { content: ""; display: block; height: 14px; width: min(180px, 70%%); border-radius: 999px; background: linear-gradient(90deg, var(--border), color-mix(in srgb, var(--border) 45%%, var(--panel)), var(--border)); background-size: 220%% 100%%; animation: loading-row 1.1s ease-in-out infinite; }\n'
        printf '        tbody tr.status-loading td:nth-child(1)::after { width: 72px; }\n'
        printf '        tbody tr.status-loading td:nth-child(3)::after { width: 96px; }\n'
        printf '        tbody tr.status-loading td:nth-child(4)::after { width: 132px; }\n'
        printf '        tbody tr.status-loading td:nth-child(5)::after { width: 132px; }\n'
        printf '        tbody tr.status-loading td:nth-child(6)::after { width: 56px; }\n'
        printf '        tbody tr.status-loading td:nth-child(7)::after { width: 84px; }\n'
        printf '        tbody tr.status-loading .actions { visibility: hidden; }\n'
        printf '        @keyframes loading-row { 0%% { background-position: 120%% 0; } 100%% { background-position: -120%% 0; } }\n'
        printf '        code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }\n'
        printf '        a { color: var(--link); font-weight: 650; text-decoration: none; }\n'
        printf '        a:hover { text-decoration: underline; }\n'
        printf '        .link-tools { display: inline-flex; align-items: center; gap: 5px; max-width: 100%%; }\n'
        printf '        .copy-button { appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--muted); font: 14px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; }\n'
        printf '        .copy-button:hover { border-color: var(--border); background: color-mix(in srgb, var(--link) 8%%, transparent); color: var(--link); }\n'
        printf '        .copy-button.is-copied { color: var(--success-text); }\n'
        printf '        .artifact-link { display: inline-flex; align-items: center; justify-content: center; }\n'
        printf '        .artifact-link svg { display: block; }\n'
        printf '        .actions-column, .actions-cell { width: 96px; min-width: 96px; }\n'
        printf '        .actions { display: inline-flex; align-items: center; justify-content: center; gap: 6px; width: 68px; }\n'
        printf '        .action-form { margin: 0; }\n'
        printf '        .action-button { appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--link); font: 20px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; }\n'
        printf '        .action-button:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); }\n'
        printf '        .action-button:disabled { color: var(--muted); cursor: default; }\n'
        printf '        .delete-button { color: var(--failed-text); }\n'
        printf '        .status { display: inline-flex; align-items: center; min-width: 72px; justify-content: center; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; text-transform: uppercase; }\n'
        printf '        .status-success, .status-passed { background: var(--success-bg); color: var(--success-text); }\n'
        printf '        .status-failed { background: var(--failed-bg); color: var(--failed-text); }\n'
        printf '        .status-running { background: var(--running-bg); color: var(--running-text); }\n'
        printf '        .status-pending { background: var(--pending-bg); color: var(--pending-text); }\n'
        printf '        .status-interrupted { background: var(--interrupted-bg); color: var(--interrupted-text); }\n'
        printf '        .status-cancelled { background: var(--cancelled-bg); color: var(--cancelled-text); }\n'
        printf '        .status-unknown { background: var(--unknown-bg); color: var(--unknown-text); }\n'
        printf '        .branch { font-weight: 650; }\n'
        printf '        .duration-cell { white-space: nowrap; }\n'
        printf '        .running-duration { display: inline-flex; align-items: center; gap: 5px; }\n'
        printf '        .running-duration-indicator { color: var(--running-text); font-size: 12px; line-height: 1; }\n'
        printf '        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }\n'
        printf '        .view-row-actions { margin-left: auto; }\n'
        printf '        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }\n'
        printf '        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }\n'
        printf '        .view-toggle span { background: var(--link); color: white; }\n'
        printf '        .view-toggle a { color: var(--link); }\n'
        printf '        .view-toggle a:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); text-decoration: none; }\n'
        printf '        .reload-form { margin: 0; }\n'
        printf '        .reload-button { appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--link); font: 21px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; }\n'
        printf '        .reload-button:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); }\n'
        printf '        .empty { padding: 28px 14px; color: var(--muted); text-align: center; }\n'
        printf '        .site-footer { width: min(1180px, calc(100%% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }\n'
        printf '        @media (max-width: 680px) {\n'
        printf '            body { font-size: 14px; }\n'
        printf '            main { margin: 16px auto; }\n'
        printf '            h1 { font-size: 22px; }\n'
        printf '            .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; }\n'
        printf '            .table-wrap { overflow-x: visible; border: none; border-radius: 0; background: transparent; box-shadow: none; }\n'
        printf '            table, thead, tbody, tr, td { display: block; }\n'
        printf '            table { min-width: 0; }\n'
        printf '            thead { display: none; }\n'
        printf '            tbody { display: flex; flex-direction: column; gap: 12px; }\n'
        printf '            tbody tr { border: 1px solid var(--border); border-radius: 10px; background: var(--panel); overflow: hidden; box-shadow: 0 2px 8px rgb(15 23 42 / 0.07); }\n'
        printf '            tbody tr:nth-child(even) { background: var(--panel); }\n'
        printf '            tbody tr:hover { background: color-mix(in srgb, var(--link) 5%%, var(--panel)); }\n'
        printf '            tbody tr.status-loading td::after { display: none; }\n'
        printf '            tbody tr.status-loading td > * { visibility: visible; }\n'
        printf '            tbody tr.status-loading { color: var(--text); }\n'
        printf '            td { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }\n'
        printf '            td + td { border-top: 1px solid color-mix(in srgb, var(--border) 50%%, transparent); }\n'
        printf '            td[data-label]::before { content: attr(data-label); width: 90px; flex-shrink: 0; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--muted); }\n'
        printf '            .actions-cell { justify-content: center; background: color-mix(in srgb, var(--border) 18%%, transparent); padding: 10px 14px; }\n'
        printf '            .actions-cell .actions { width: auto; }\n'
        printf '            .actions-cell .action-button { width: 40px; height: 40px; font-size: 22px; }\n'
        printf '        }\n'
        printf '    </style>\n'
        printf '</head>\n'
        printf '<body>\n'
        printf '    <main>\n'
        printf '        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>%s</h1>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >>"$index_file"
    write_build_artifact_view_toggle "$index_file" "$current_view_label" "$reload_action_html"
    {
        printf '        <div class="table-wrap">\n'
        printf '            <table>\n'
        printf '                <thead><tr><th>Status</th><th><button class="sort-button" type="button" data-sort="branch">Branch</button></th><th>Commit</th><th><button class="sort-button" type="button" data-sort="commit-time">Commit Time</button></th><th><button class="sort-button" type="button" data-sort="status-time">Status Time</button></th><th>Duration</th><th>Artifacts</th><th class="actions-column">Actions</th></tr></thead>\n'
        printf '                <tbody id="build-rows">\n'
    } >>"$index_file"

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        if [ -z "$branch" ]; then
            continue
        fi
        has_results=true
        normalize_build_result_fields
        if [ "$status" = unknown ]; then
            artifact_key=
        fi
            commit_abbrev=${commit:0:12}
            local_status="$status"
            loading_status=false
            if [ "$load_statuses_in_browser" = true ]; then
                case "$local_status" in
                    pending|interrupted|cancelled)
                        display_status="$local_status"
                        ;;
                    *)
                        display_status=loading
                        loading_status=true
                        ;;
                esac
            else
                display_status=$(effective_build_status "$commit" "$local_status")
            fi
            status_class="status-$display_status"
            row_class="$status_class"
            if [ "$loading_status" = true ]; then
                row_class="status-loading"
            fi
            commit_timestamp_value=$(commit_timestamp "$commit")
            display_commit_timestamp=$(display_build_timestamp "$commit_timestamp_value")
            display_status_timestamp=$(display_build_timestamp "$timestamp")
            branch_cell=$(printf '%s' "$branch" | html_escape)
            branch_copy_button=
            branch_url=$(gitea_branch_web_url "$branch")
            if [ -n "$branch_url" ]; then
                branch_copy_button=$(html_copy_button "$branch" "branch name")
                branch_cell=$(printf '<span class="link-tools"><a href="%s" target="_blank" rel="noopener noreferrer">%s</a>%s</span>' \
                    "$(printf '%s' "$branch_url" | html_escape)" \
                    "$branch_cell" \
                    "$branch_copy_button")
            fi
            commit_cell=$(printf '%s' "$commit_abbrev" | html_escape)
            commit_copy_button=
            commit_url=
            if [[ "$commit" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
                commit_url=$(gitea_commit_web_url "$commit")
            fi
            if [ -n "$commit_url" ]; then
                commit_copy_button=$(html_copy_button "$commit" "full commit ID")
                commit_cell=$(printf '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>' \
                    "$(printf '%s' "$commit_url" | html_escape)" \
                    "$commit_cell")
            fi
            printf '                    <tr class="%s" data-row-index="%s" data-branch="%s" data-commit-time="%s" data-status-time="%s" data-artifact-key="%s" data-commit="%s" data-local-status="%s"><td data-label="Status"><span class="status %s">%s</span></td><td class="branch" data-label="Branch">%s</td><td data-label="Commit"><span class="link-tools"><code>%s</code>%s</span></td><td data-label="Commit Time">%s</td><td data-label="Status Time">%s</td><td class="duration-cell" data-label="Duration" data-duration="%s">%s</td><td data-label="Artifacts">' \
                "$(printf '%s' "$row_class" | html_escape)" \
                "$row_index" \
                "$(printf '%s' "$branch" | html_escape)" \
                "$(printf '%s' "$commit_timestamp_value" | html_escape)" \
                "$(printf '%s' "$timestamp" | html_escape)" \
                "$(printf '%s' "$artifact_key" | html_escape)" \
                "$(printf '%s' "$commit" | html_escape)" \
                "$(printf '%s' "$local_status" | html_escape)" \
                "$(printf '%s' "$status_class" | html_escape)" \
                "$(printf '%s' "$display_status" | html_escape)" \
                "$branch_cell" \
                "$commit_cell" \
                "$commit_copy_button" \
                "$(printf '%s' "$display_commit_timestamp" | html_escape)" \
                "$(printf '%s' "$display_status_timestamp" | html_escape)" \
                "$(printf '%s' "$duration" | html_escape)" \
                "$(printf '%s' "$duration" | html_escape)" \
                >>"$index_file"
            row_index=$((row_index + 1))
            if [ -n "$artifact_key" ] &&
                { [ -f "$artifacts_root/branches/$artifact_key/index.html" ] ||
                  [ -f "$(build_artifact_index_content_file "$artifacts_root/branches/$artifact_key")" ]; }; then
                printf '<a class="artifact-link" href="branches/%s/index.html" title="Open artifacts"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></a>' \
                    "$(printf '%s' "$artifact_key" | html_escape)" \
                    >>"$index_file"
            else
                printf 'n/a' >>"$index_file"
            fi
            printf '</td><td class="actions-cell"><div class="actions">' >>"$index_file"
            if [ "$use_artifact_http_server" = true ]; then
                if [ "$view" = "history" ]; then
                    return_to=history.html
                elif [ "$view" = "branches" ]; then
                    return_to=branches.html
                else
                    return_to=index.html
                fi
                if [ "$view" != "history" ]; then
                    printf '<form class="action-form" method="post" action="/control/restart"><input type="hidden" name="branch" value="%s"><input type="hidden" name="commit" value="%s"><input type="hidden" name="return_to" value="%s"><button class="action-button" type="submit" title="Restart build" aria-label="Restart build">↻</button></form>' \
                        "$(printf '%s' "$branch" | html_escape)" \
                        "$(printf '%s' "$commit" | html_escape)" \
                        "$return_to" \
                        >>"$index_file"
                fi
                if [ -n "$artifact_key" ]; then
                    printf '<form class="action-form" method="post" action="/control/delete"><input type="hidden" name="branch" value="%s"><input type="hidden" name="commit" value="%s"><input type="hidden" name="artifact_key" value="%s"><input type="hidden" name="return_to" value="%s"><button class="action-button delete-button" type="submit" title="Delete stored status" aria-label="Delete stored status">×</button></form>' \
                        "$(printf '%s' "$branch" | html_escape)" \
                        "$(printf '%s' "$commit" | html_escape)" \
                        "$(printf '%s' "$artifact_key" | html_escape)" \
                        "$return_to" \
                        >>"$index_file"
                fi
            fi
            printf '</div></td></tr>\n' >>"$index_file"
    done < <(
        if [ "$view" = "latest" ]; then
            if [ -f "$results_file" ]; then
                sort -t $'\t' -k4,4r "$results_file" | awk -F '\t' '!seen[$1]++'
            fi
        elif [ "$view" = "branches" ]; then
            git for-each-ref --format='%(refname:strip=2)%09%(objectname)' refs/heads |
                awk -F '\t' '{
                    sort_group = 2
                    if ($1 == "main" || $1 == "master") {
                        sort_group = 0
                    } else if (index($1, "/") == 0) {
                        sort_group = 1
                    }
                    printf "%d\t%s\t%s\n", sort_group, $1, $0
                }' |
                sort -t $'\t' -k1,1n -k2,2 |
                cut -f3- |
                while IFS=$'\t' read -r branch commit; do
                    result_line=
                    if [ -f "$results_file" ]; then
                        result_line=$(sort -t $'\t' -k4,4r "$results_file" | awk -F '\t' -v wanted="$branch" '$1 == wanted { print; exit }')
                    fi
                    if [ -n "$result_line" ]; then
                        printf '%s\n' "$result_line"
                    else
                        printf '%s\t%s\tunknown\t\t\t\n' "$branch" "$commit"
                    fi
                done
        elif [ -f "$results_file" ]; then
                sort -t $'\t' -k4,4r "$results_file"
        fi
    )

    if [ "$has_results" = false ]; then
        if [ "$view" = "latest" ]; then
            printf '                    <tr><td class="empty" colspan="8">No latest build results found.</td></tr>\n' >>"$index_file"
        elif [ "$view" = "branches" ]; then
            printf '                    <tr><td class="empty" colspan="8">No local branches found.</td></tr>\n' >>"$index_file"
        else
            printf '                    <tr><td class="empty" colspan="8">No builds archived yet.</td></tr>\n' >>"$index_file"
        fi
    fi

    {
        printf '                </tbody>\n'
        printf '            </table>\n'
        printf '        </div>\n'
        printf '    </main>\n'
    } >>"$index_file"
    {
        printf '    <script>\n'
        printf '        const buildRows = document.getElementById("build-rows");\n'
        printf '        const loadStatusesInBrowser = %s;\n' "$load_statuses_in_browser"
        printf '        const knownStatuses = new Set(["success", "passed", "failed", "running", "pending", "interrupted", "cancelled", "unknown"]);\n'
        printf '        const terminalStatuses = new Set(["success", "passed", "failed", "cancelled", "interrupted"]);\n'
        printf '        const resolvedStatusCache = new Map();\n'
        printf '        let activeSort = { key: "", direction: "" };\n'
        printf '        let serverRowsHtml = buildRows ? buildRows.innerHTML : "";\n'
        printf '        let isRefreshingView = false;\n'
        printf '        const statusLoadingRows = new WeakSet();\n'
        printf '        function branchSortKey(row) {\n'
        printf '            const branch = row.dataset.branch || "";\n'
        printf '            let group = 2;\n'
        printf '            if (branch === "main" || branch === "master") group = 0;\n'
        printf '            else if (!branch.includes("/")) group = 1;\n'
        printf '            return [group, branch.toLocaleLowerCase(), branch];\n'
        printf '        }\n'
        printf '        function timeSortValue(row, key) {\n'
        printf '            const value = key === "commit-time" ? row.dataset.commitTime : row.dataset.statusTime;\n'
        printf '            const time = value ? Date.parse(value) : Number.NaN;\n'
        printf '            return Number.isNaN(time) ? 0 : time;\n'
        printf '        }\n'
        printf '        function compareRows(left, right, key, direction) {\n'
        printf '            let result = 0;\n'
        printf '            if (key === "branch") {\n'
        printf '                const leftKey = branchSortKey(left);\n'
        printf '                const rightKey = branchSortKey(right);\n'
        printf '                result = leftKey[0] - rightKey[0] || leftKey[1].localeCompare(rightKey[1]) || leftKey[2].localeCompare(rightKey[2]);\n'
        printf '            } else {\n'
        printf '                result = timeSortValue(left, key) - timeSortValue(right, key);\n'
        printf '            }\n'
        printf '            if (result === 0) {\n'
        printf '                result = Number(left.dataset.rowIndex || 0) - Number(right.dataset.rowIndex || 0);\n'
        printf '            }\n'
        printf '            return direction === "desc" ? -result : result;\n'
        printf '        }\n'
        printf '        function updateSortButtons() {\n'
        printf '            document.querySelectorAll(".sort-button[data-sort]").forEach((button) => {\n'
        printf '                const isActive = button.dataset.sort === activeSort.key;\n'
        printf '                button.classList.toggle("is-active", isActive);\n'
        printf '                button.dataset.sortDirection = isActive ? activeSort.direction : "";\n'
        printf '            });\n'
        printf '        }\n'
        printf '        function sortRows(key, direction) {\n'
        printf '            if (!buildRows) return;\n'
        printf '            activeSort = { key, direction };\n'
        printf '            const rows = Array.from(buildRows.querySelectorAll("tr[data-branch]"));\n'
        printf '            rows.sort((left, right) => compareRows(left, right, key, direction));\n'
        printf '            rows.forEach((row) => buildRows.appendChild(row));\n'
        printf '            updateSortButtons();\n'
        printf '        }\n'
        printf '        function rowCacheKey(row) {\n'
        printf '            return (row.dataset.commit || "") + ":" + (row.dataset.artifactKey || "");\n'
        printf '        }\n'
        printf '        function applyStatus(row, status) {\n'
        printf '            const normalized = knownStatuses.has(status) ? status : row.dataset.localStatus;\n'
        printf '            const statusCell = row.querySelector(".status");\n'
        printf '            if (!statusCell) return;\n'
        printf '            row.className = "status-" + normalized;\n'
        printf '            statusCell.className = "status status-" + normalized;\n'
        printf '            statusCell.textContent = normalized;\n'
        printf '            updateRunningDuration(row);\n'
        printf '            if (terminalStatuses.has(normalized)) resolvedStatusCache.set(rowCacheKey(row), normalized);\n'
        printf '        }\n'
        printf '        function applyCachedStatuses() {\n'
        printf '            if (!buildRows) return;\n'
        printf '            buildRows.querySelectorAll("tr.status-loading[data-commit]").forEach(row => {\n'
        printf '                const cached = resolvedStatusCache.get(rowCacheKey(row));\n'
        printf '                if (cached) applyStatus(row, cached);\n'
        printf '            });\n'
        printf '        }\n'
        printf '        function formatDuration(seconds) {\n'
        printf '            const safeSeconds = Math.max(0, Math.floor(seconds));\n'
        printf '            const minutes = Math.floor(safeSeconds / 60);\n'
        printf '            const remainingSeconds = safeSeconds %% 60;\n'
        printf '            return String(minutes).padStart(2, "0") + ":" + String(remainingSeconds).padStart(2, "0");\n'
        printf '        }\n'
        printf '        function rowStatus(row) {\n'
        printf '            const statusCell = row.querySelector(".status");\n'
        printf '            return statusCell ? statusCell.textContent.trim() : row.dataset.localStatus;\n'
        printf '        }\n'
        printf '        function updateRunningDuration(row) {\n'
        printf '            const durationCell = row.querySelector(".duration-cell");\n'
        printf '            if (!durationCell) return;\n'
        printf '            if (rowStatus(row) !== "running") {\n'
        printf '                durationCell.textContent = durationCell.dataset.duration || "";\n'
        printf '                return;\n'
        printf '            }\n'
        printf '            const startedAt = Date.parse(row.dataset.statusTime || "");\n'
        printf '            if (Number.isNaN(startedAt)) return;\n'
        printf '            const elapsedSeconds = (Date.now() - startedAt) / 1000;\n'
        printf '            durationCell.innerHTML = "";\n'
        printf '            const wrapper = document.createElement("span");\n'
        printf '            wrapper.className = "running-duration";\n'
        printf '            const value = document.createElement("span");\n'
        printf '            value.textContent = formatDuration(elapsedSeconds);\n'
        printf '            const indicator = document.createElement("span");\n'
        printf '            indicator.className = "running-duration-indicator";\n'
        printf '            indicator.title = "Build is still running";\n'
        printf '            indicator.setAttribute("aria-label", "Build is still running");\n'
        printf '            indicator.textContent = "⏱";\n'
        printf '            wrapper.append(value, indicator);\n'
        printf '            durationCell.append(wrapper);\n'
        printf '        }\n'
        printf '        function updateRunningDurations() {\n'
        printf '            if (!buildRows) return;\n'
        printf '            buildRows.querySelectorAll("tr[data-status-time]").forEach(updateRunningDuration);\n'
        printf '        }\n'
        printf '        async function loadStatus(row) {\n'
        printf '            if (!loadStatusesInBrowser || !row.dataset.commit || !/^[0-9a-fA-F]{7,40}$/.test(row.dataset.commit)) return;\n'
        printf '            if (!row.classList.contains("status-loading")) return;\n'
        printf '            if (statusLoadingRows.has(row)) return;\n'
        printf '            statusLoadingRows.add(row);\n'
        printf '            const params = new URLSearchParams({ commit: row.dataset.commit, local_status: row.dataset.localStatus || "unknown" });\n'
        printf '            const controller = new AbortController();\n'
        printf '            const timeoutId = window.setTimeout(() => controller.abort(), 15000);\n'
        printf '            try {\n'
        printf '                const response = await fetch("/control/status?" + params.toString(), { cache: "no-store", signal: controller.signal });\n'
        printf '                const body = response.ok ? await response.json() : null;\n'
        printf '                applyStatus(row, body && body.status ? body.status : row.dataset.localStatus);\n'
        printf '            } catch (error) {\n'
        printf '                applyStatus(row, row.dataset.localStatus);\n'
        printf '            } finally {\n'
        printf '                window.clearTimeout(timeoutId);\n'
        printf '                statusLoadingRows.delete(row);\n'
        printf '            }\n'
        printf '        }\n'
        printf '        function loadVisibleStatuses() {\n'
        printf '            if (!loadStatusesInBrowser || !buildRows) return;\n'
        printf '            Array.from(buildRows.querySelectorAll("tr[data-commit]")).forEach(loadStatus);\n'
        printf '        }\n'
        printf '        async function copyToClipboard(button) {\n'
        printf '            const text = button.dataset.copy || "";\n'
        printf '            if (!text) return;\n'
        printf '            try {\n'
        printf '                if (navigator.clipboard && window.isSecureContext) {\n'
        printf '                    await navigator.clipboard.writeText(text);\n'
        printf '                } else {\n'
        printf '                    const input = document.createElement("textarea");\n'
        printf '                    input.value = text;\n'
        printf '                    input.style.position = "fixed";\n'
        printf '                    input.style.left = "-9999px";\n'
        printf '                    document.body.appendChild(input);\n'
        printf '                    input.focus();\n'
        printf '                    input.select();\n'
        printf '                    document.execCommand("copy");\n'
        printf '                    input.remove();\n'
        printf '                }\n'
        printf '                button.classList.add("is-copied");\n'
        printf '                window.setTimeout(() => button.classList.remove("is-copied"), 900);\n'
        printf '            } catch (error) {\n'
        printf '                return;\n'
        printf '            }\n'
        printf '        }\n'
        printf '        function replaceRowsSmoothly(nextRowsHtml) {\n'
        printf '            if (!buildRows) return;\n'
        printf '            buildRows.classList.add("is-refreshing");\n'
        printf '            window.setTimeout(() => {\n'
        printf '                buildRows.innerHTML = nextRowsHtml;\n'
        printf '                if (activeSort.key) sortRows(activeSort.key, activeSort.direction);\n'
        printf '                applyCachedStatuses();\n'
        printf '                loadVisibleStatuses();\n'
        printf '                updateRunningDurations();\n'
        printf '                window.requestAnimationFrame(() => buildRows.classList.remove("is-refreshing"));\n'
        printf '            }, 140);\n'
        printf '        }\n'
        printf '        async function refreshViewIfChanged() {\n'
        printf '            if (!buildRows || isRefreshingView) return;\n'
        printf '            isRefreshingView = true;\n'
        printf '            try {\n'
        printf '                const response = await fetch(window.location.pathname + "?ts=" + Date.now(), { cache: "no-store" });\n'
        printf '                if (!response.ok) return;\n'
        printf '                const html = await response.text();\n'
        printf '                const doc = new DOMParser().parseFromString(html, "text/html");\n'
        printf '                const nextRows = doc.getElementById("build-rows");\n'
        printf '                if (!nextRows) return;\n'
        printf '                const nextRowsHtml = nextRows.innerHTML;\n'
        printf '                if (nextRowsHtml !== serverRowsHtml) {\n'
        printf '                    serverRowsHtml = nextRowsHtml;\n'
        printf '                    replaceRowsSmoothly(nextRowsHtml);\n'
        printf '                } else {\n'
        printf '                    loadVisibleStatuses();\n'
        printf '                }\n'
        printf '            } catch (error) {\n'
        printf '                return;\n'
        printf '            } finally {\n'
        printf '                isRefreshingView = false;\n'
        printf '            }\n'
        printf '        }\n'
        printf '        loadVisibleStatuses();\n'
        printf '        updateRunningDurations();\n'
        printf '        document.addEventListener("click", (event) => {\n'
        printf '            const button = event.target.closest(".copy-button[data-copy]");\n'
        printf '            if (!button) return;\n'
        printf '            event.preventDefault();\n'
        printf '            copyToClipboard(button);\n'
        printf '        });\n'
        printf '        document.addEventListener("click", (event) => {\n'
        printf '            const button = event.target.closest(".sort-button[data-sort]");\n'
        printf '            if (!button) return;\n'
        printf '            const key = button.dataset.sort;\n'
        printf '            let direction = key === "branch" ? "asc" : "desc";\n'
        printf '            if (activeSort.key === key) {\n'
        printf '                direction = activeSort.direction === "asc" ? "desc" : "asc";\n'
        printf '            }\n'
        printf '            sortRows(key, direction);\n'
        printf '        });\n'
        printf '        updateSortButtons();\n'
        printf '        let runningDurationTimer = null;\n'
        printf '        let refreshViewTimer = null;\n'
        printf '        function startAutoRefresh() {\n'
        printf '            if (runningDurationTimer === null) runningDurationTimer = window.setInterval(updateRunningDurations, 1000);\n'
        printf '            if (refreshViewTimer === null) refreshViewTimer = window.setInterval(refreshViewIfChanged, 15000);\n'
        printf '        }\n'
        printf '        function stopAutoRefresh() {\n'
        printf '            if (runningDurationTimer !== null) window.clearInterval(runningDurationTimer);\n'
        printf '            if (refreshViewTimer !== null) window.clearInterval(refreshViewTimer);\n'
        printf '            runningDurationTimer = null;\n'
        printf '            refreshViewTimer = null;\n'
        printf '        }\n'
        printf '        window.addEventListener("pageshow", () => { updateRunningDurations(); refreshViewIfChanged(); startAutoRefresh(); });\n'
        printf '        window.addEventListener("pagehide", stopAutoRefresh);\n'
        printf '        startAutoRefresh();\n'
        printf '    </script>\n'
    } >>"$index_file"
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

current_build_log_file() {
    echo "$(build_artifacts_root)/current.log"
}

write_current_build_page() {
    local branch="${1:-}"
    local status="${2:-idle}"
    local timestamp="${3:-}"
    local artifacts_root
    local index_file
    local page_title
    local display_timestamp
    local cancel_token
    local cancel_action_html=

    artifacts_root=$(build_artifacts_root)
    write_html_favicon "$artifacts_root" || return 1
    write_script_download "$artifacts_root" || return 1
    write_html_about_page "$artifacts_root" || return 1
    write_html_license_page "$artifacts_root" || return 1
    index_file="$artifacts_root/current.html"
    page_title="GitTally [$(repository_simple_name)] - Current Branch Build"
    display_timestamp=$(display_build_timestamp "$timestamp")
    if [ "$status" = running ]; then
        cancel_token=$(read_build_cancel_token)
        if [ -n "$cancel_token" ]; then
            cancel_action_html=$(printf '<form id="cancel-form" class="cancel-form" method="post" action="/control/cancel"><input type="hidden" name="token" value="%s"><button class="cancel-button" type="submit">Cancel build</button><span id="cancel-status" class="cancel-status"></span></form>' "$(printf '%s' "$cancel_token" | html_escape)")
        fi
    fi
    mkdir -p "$artifacts_root" || return 1

    {
        printf '<!doctype html>\n'
        printf '<html lang="en">\n'
        printf '<head>\n'
        printf '    <meta charset="utf-8">\n'
        printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
        printf '    <meta http-equiv="Cache-Control" content="no-store">\n'
        printf '    <meta http-equiv="Pragma" content="no-cache">\n'
        printf '    <meta http-equiv="Expires" content="0">\n'
        printf '    <title>%s</title>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >"$index_file"
    write_html_favicon_links "$index_file"
    {
        printf '    <style>\n'
        printf '        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --link: #155eef; }\n'
        printf '        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --link: #93c5fd; } }\n'
        printf '        * { box-sizing: border-box; }\n'
        printf '        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n'
        printf '        main { width: min(1180px, calc(100%% - 32px)); margin: 32px auto; }\n'
        printf '        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }\n'
        printf '        h1 img { width: 32px; height: 32px; flex: none; }\n'
        printf '        .title-home { display: inline-flex; flex: none; }\n'
        printf '        a { color: var(--link); font-weight: 650; text-decoration: none; }\n'
        printf '        a:hover { text-decoration: underline; }\n'
        printf '        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }\n'
        printf '        .view-row-actions { margin-left: auto; }\n'
        printf '        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }\n'
        printf '        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }\n'
        printf '        .view-toggle span { background: var(--link); color: white; }\n'
        printf '        .view-toggle a { color: var(--link); }\n'
        printf '        .meta { margin: 0 0 14px; color: var(--muted); }\n'
        printf '        .cancel-form { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin: 0; }\n'
        printf '        .cancel-button { appearance: none; border: 1px solid #991b1b; border-radius: 6px; background: #dc2626; color: white; padding: 7px 12px; font: inherit; font-weight: 700; cursor: pointer; }\n'
        printf '        .cancel-button:hover { background: #b91c1c; }\n'
        printf '        .cancel-status { color: var(--muted); }\n'
        printf '        .log { min-height: 65vh; max-height: 72vh; overflow: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); padding: 14px; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n'
        printf '        .site-footer { width: min(1180px, calc(100%% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }\n'
        printf '        @media (max-width: 680px) { body { font-size: 14px; } main { margin: 16px auto; } h1 { font-size: 22px; } .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; } .log { font-size: 13px; min-height: 50vh; max-height: 60vh; } }\n'
        printf '    </style>\n'
        printf '</head>\n'
        printf '<body>\n'
        printf '    <main>\n'
        printf '        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>%s</h1>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >>"$index_file"
    write_build_artifact_view_toggle "$index_file" Current "$cancel_action_html"
    {
        if [ -n "$branch" ]; then
            printf '        <p class="meta">Status: %s | Branch: %s' \
                "$(printf '%s' "$status" | html_escape)" \
                "$(printf '%s' "$branch" | html_escape)"
            if [ -n "$display_timestamp" ]; then
                printf ' | Started: %s' "$(printf '%s' "$display_timestamp" | html_escape)"
            fi
            printf '</p>\n'
        else
            printf '        <p class="meta">No build is currently running.</p>\n'
        fi
        printf '        <pre id="log" class="log">Loading current.log...</pre>\n'
        printf '    </main>\n'
        printf '    <script>\n'
        printf '        const log = document.getElementById("log");\n'
        printf '        const cancelForm = document.getElementById("cancel-form");\n'
        printf '        const cancelStatus = document.getElementById("cancel-status");\n'
        printf '        let previous = "";\n'
        printf '        async function refreshLog() {\n'
        printf '            try {\n'
        printf '                const response = await fetch("current.log?ts=" + Date.now(), { cache: "no-store" });\n'
        printf '                const text = response.ok ? await response.text() : "";\n'
        printf '                if (text !== previous) {\n'
        printf '                    previous = text;\n'
        printf '                    log.textContent = text || "No build output available.";\n'
        printf '                    log.scrollTop = log.scrollHeight;\n'
        printf '                }\n'
        printf '            } catch (error) {\n'
        printf '                log.textContent = "Could not load current.log.";\n'
        printf '            }\n'
        printf '        }\n'
        printf '        if (cancelForm) {\n'
        printf '            cancelForm.addEventListener("submit", async (event) => {\n'
        printf '                event.preventDefault();\n'
        printf '                cancelStatus.textContent = "Requesting cancellation...";\n'
        printf '                try {\n'
        printf '                    const response = await fetch(cancelForm.action, { method: "POST", body: new URLSearchParams(new FormData(cancelForm)), cache: "no-store" });\n'
        printf '                    cancelStatus.textContent = response.ok ? "Cancellation requested." : "Cancellation request failed.";\n'
        printf '                } catch (error) {\n'
        printf '                    cancelStatus.textContent = "Cancellation request failed.";\n'
        printf '                }\n'
        printf '            });\n'
        printf '        }\n'
        printf '        let refreshLogTimer = null;\n'
        printf '        function startLogAutoRefresh() {\n'
        printf '            if (refreshLogTimer === null) refreshLogTimer = window.setInterval(refreshLog, 2000);\n'
        printf '        }\n'
        printf '        function stopLogAutoRefresh() {\n'
        printf '            if (refreshLogTimer !== null) window.clearInterval(refreshLogTimer);\n'
        printf '            refreshLogTimer = null;\n'
        printf '        }\n'
        printf '        window.addEventListener("pageshow", () => { refreshLog(); startLogAutoRefresh(); });\n'
        printf '        window.addEventListener("pagehide", stopLogAutoRefresh);\n'
        printf '        refreshLog();\n'
        printf '        startLogAutoRefresh();\n'
        printf '    </script>\n'
    } >>"$index_file"
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

system_page_snapshot() {
    local system_file="$1"

    python3 - "$system_file" <<'PY'
import datetime
import json
import math
import sys

placeholder = "\u2014"
keys = [
    "cpu_used", "cpu_used_min", "cpu_used_max", "cpu_used_avg",
    "cpu_idle", "cpu_idle_min", "cpu_idle_max", "cpu_idle_avg",
    "ram_used_gib", "ram_used_gib_min", "ram_used_gib_max", "ram_used_gib_avg",
    "ram_free_gib", "ram_free_gib_min", "ram_free_gib_max", "ram_free_gib_avg",
]

def fmt(value):
    if isinstance(value, (int, float)) and math.isfinite(value):
        return f"{value:.2f}"
    return placeholder

try:
    with open(sys.argv[1], encoding="utf-8") as input_file:
        data = json.load(input_file)
except Exception:
    data = {}

values = [fmt(data.get(key)) for key in keys]
cpu_count = data.get("cpu_count")
values.append(f"{cpu_count} cores" if isinstance(cpu_count, int) else placeholder)
values.append(fmt(data.get("ram_total_gib")) + " GiB" if isinstance(data.get("ram_total_gib"), (int, float)) else placeholder)
timestamp = data.get("timestamp")
if isinstance(timestamp, str) and timestamp:
    try:
        values.append(datetime.datetime.fromisoformat(timestamp).strftime("%H:%M:%S"))
    except ValueError:
        values.append(timestamp)
else:
    values.append(placeholder)
print("\t".join(values))
PY
}

write_system_page() {
    local artifacts_root
    local index_file
    local page_title
    local cpu_used cpu_used_min cpu_used_max cpu_used_avg
    local cpu_idle cpu_idle_min cpu_idle_max cpu_idle_avg
    local ram_used ram_used_min ram_used_max ram_used_avg
    local ram_free ram_free_min ram_free_max ram_free_avg
    local cpu_count ram_total updated

    artifacts_root=$(build_artifacts_root)
    write_html_favicon "$artifacts_root" || return 1
    index_file="$artifacts_root/system.html"
    page_title="GitTally [$(repository_simple_name)] - System"
    mkdir -p "$artifacts_root" || return 1
    IFS=$'\t' read -r \
        cpu_used cpu_used_min cpu_used_max cpu_used_avg \
        cpu_idle cpu_idle_min cpu_idle_max cpu_idle_avg \
        ram_used ram_used_min ram_used_max ram_used_avg \
        ram_free ram_free_min ram_free_max ram_free_avg \
        cpu_count ram_total updated < <(system_page_snapshot "$artifacts_root/system.json")

    {
        printf '<!doctype html>\n'
        printf '<html lang="en">\n'
        printf '<head>\n'
        printf '    <meta charset="utf-8">\n'
        printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
        printf '    <meta http-equiv="Cache-Control" content="no-store">\n'
        printf '    <meta http-equiv="Pragma" content="no-cache">\n'
        printf '    <meta http-equiv="Expires" content="0">\n'
        printf '    <title>%s</title>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >"$index_file"
    write_html_favicon_links "$index_file"
    {
        printf '    <style>\n'
        printf '        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --row: #f9fafb; --link: #155eef; }\n'
        printf '        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --row: #182235; --link: #93c5fd; } }\n'
        printf '        * { box-sizing: border-box; }\n'
        printf '        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n'
        printf '        main { width: min(1180px, calc(100%% - 32px)); margin: 32px auto; }\n'
        printf '        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }\n'
        printf '        h1 img { width: 32px; height: 32px; flex: none; }\n'
        printf '        .title-home { display: inline-flex; flex: none; }\n'
        printf '        a { color: var(--link); font-weight: 650; text-decoration: none; }\n'
        printf '        a:hover { text-decoration: underline; }\n'
        printf '        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }\n'
        printf '        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }\n'
        printf '        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }\n'
        printf '        .view-toggle span { background: var(--link); color: white; }\n'
        printf '        .view-toggle a { color: var(--link); }\n'
        printf '        .view-toggle a:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); text-decoration: none; }\n'
        printf '        .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 28px rgb(15 23 42 / 0.08); }\n'
        printf '        table { width: 100%%; border-collapse: collapse; }\n'
        printf '        th, td { padding: 12px 14px; text-align: left; vertical-align: middle; border-bottom: 1px solid var(--border); }\n'
        printf '        th { background: var(--panel); color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: 0; text-transform: uppercase; }\n'
        printf '        tbody tr:nth-child(even) { background: var(--row); }\n'
        printf '        tbody tr:last-child td { border-bottom: 0; }\n'
        printf '        .num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }\n'
        printf '        .meta { margin: 14px 0 0; color: var(--muted); font-size: 13px; display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 6px; }\n'
        printf '        .site-footer { width: min(1180px, calc(100%% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }\n'
        printf '        @media (max-width: 680px) {\n'
        printf '            body { font-size: 14px; }\n'
        printf '            main { margin: 16px auto; }\n'
        printf '            h1 { font-size: 22px; }\n'
        printf '            .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; }\n'
        printf '            .table-wrap { overflow-x: visible; border: none; border-radius: 0; background: transparent; box-shadow: none; }\n'
        printf '            table, thead, tbody, tr, td { display: block; }\n'
        printf '            table { min-width: 0; }\n'
        printf '            thead { display: none; }\n'
        printf '            tbody { display: flex; flex-direction: column; gap: 12px; }\n'
        printf '            tbody tr { border: 1px solid var(--border); border-radius: 10px; background: var(--panel); overflow: hidden; }\n'
        printf '            tbody tr:nth-child(even) { background: var(--panel); }\n'
        printf '            td { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }\n'
        printf '            td + td { border-top: 1px solid color-mix(in srgb, var(--border) 50%%, transparent); }\n'
        printf '            td[data-label]::before { content: attr(data-label); width: 80px; flex-shrink: 0; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--muted); }\n'
        printf '            .num { text-align: left; }\n'
        printf '        }\n'
        printf '    </style>\n'
        printf '</head>\n'
        printf '<body>\n'
        printf '    <main>\n'
        printf '        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>%s</h1>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >>"$index_file"
    write_build_artifact_view_toggle "$index_file" System
    {
        printf '        <div class="table-wrap">\n'
        printf '            <table>\n'
        printf '                <thead><tr><th>Metric</th><th class="num">Current</th><th class="num">Min</th><th class="num">Max</th><th class="num">Avg</th></tr></thead>\n'
        printf '                <tbody>\n'
        printf '                    <tr><td>CPU used (cores)</td><td id="cur-cpu-used" class="num" data-label="Current">%s</td><td id="min-cpu-used" class="num" data-label="Min">%s</td><td id="max-cpu-used" class="num" data-label="Max">%s</td><td id="avg-cpu-used" class="num" data-label="Avg">%s</td></tr>\n' \
            "$(printf '%s' "$cpu_used" | html_escape)" "$(printf '%s' "$cpu_used_min" | html_escape)" "$(printf '%s' "$cpu_used_max" | html_escape)" "$(printf '%s' "$cpu_used_avg" | html_escape)"
        printf '                    <tr><td>CPU idle (cores)</td><td id="cur-cpu-idle" class="num" data-label="Current">%s</td><td id="min-cpu-idle" class="num" data-label="Min">%s</td><td id="max-cpu-idle" class="num" data-label="Max">%s</td><td id="avg-cpu-idle" class="num" data-label="Avg">%s</td></tr>\n' \
            "$(printf '%s' "$cpu_idle" | html_escape)" "$(printf '%s' "$cpu_idle_min" | html_escape)" "$(printf '%s' "$cpu_idle_max" | html_escape)" "$(printf '%s' "$cpu_idle_avg" | html_escape)"
        printf '                    <tr><td>RAM used (GiB)</td><td id="cur-ram-used" class="num" data-label="Current">%s</td><td id="min-ram-used" class="num" data-label="Min">%s</td><td id="max-ram-used" class="num" data-label="Max">%s</td><td id="avg-ram-used" class="num" data-label="Avg">%s</td></tr>\n' \
            "$(printf '%s' "$ram_used" | html_escape)" "$(printf '%s' "$ram_used_min" | html_escape)" "$(printf '%s' "$ram_used_max" | html_escape)" "$(printf '%s' "$ram_used_avg" | html_escape)"
        printf '                    <tr><td>RAM free (GiB)</td><td id="cur-ram-avail" class="num" data-label="Current">%s</td><td id="min-ram-avail" class="num" data-label="Min">%s</td><td id="max-ram-avail" class="num" data-label="Max">%s</td><td id="avg-ram-avail" class="num" data-label="Avg">%s</td></tr>\n' \
            "$(printf '%s' "$ram_free" | html_escape)" "$(printf '%s' "$ram_free_min" | html_escape)" "$(printf '%s' "$ram_free_max" | html_escape)" "$(printf '%s' "$ram_free_avg" | html_escape)"
        printf '                </tbody>\n'
        printf '            </table>\n'
        printf '        </div>\n'
        printf '        <p class="meta"><span>CPU total:&nbsp;<strong id="info-cpu-count">%s</strong>&nbsp;&nbsp;·&nbsp;&nbsp;RAM total:&nbsp;<strong id="info-ram-total">%s</strong>&nbsp;&nbsp;·&nbsp;&nbsp;Updated:&nbsp;<span id="info-updated">%s</span></span><span>(*: min/max/avg since script start)</span></p>\n' \
            "$(printf '%s' "$cpu_count" | html_escape)" "$(printf '%s' "$ram_total" | html_escape)" "$(printf '%s' "$updated" | html_escape)"
        printf '    </main>\n'
        printf '    <script>\n'
        printf '        function fmt(v) { return (typeof v === "number" && isFinite(v)) ? v.toFixed(2) : "—"; }\n'
        printf '        function set(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; }\n'
        printf '        const rows = [\n'
        printf '            { cur: "cpu_used",     min: "cpu_used_min",     max: "cpu_used_max",     avg: "cpu_used_avg",     curId: "cur-cpu-used",  minId: "min-cpu-used",  maxId: "max-cpu-used",  avgId: "avg-cpu-used"  },\n'
        printf '            { cur: "cpu_idle",     min: "cpu_idle_min",     max: "cpu_idle_max",     avg: "cpu_idle_avg",     curId: "cur-cpu-idle",  minId: "min-cpu-idle",  maxId: "max-cpu-idle",  avgId: "avg-cpu-idle"  },\n'
        printf '            { cur: "ram_used_gib", min: "ram_used_gib_min", max: "ram_used_gib_max", avg: "ram_used_gib_avg", curId: "cur-ram-used",  minId: "min-ram-used",  maxId: "max-ram-used",  avgId: "avg-ram-used"  },\n'
        printf '            { cur: "ram_free_gib", min: "ram_free_gib_min", max: "ram_free_gib_max", avg: "ram_free_gib_avg", curId: "cur-ram-avail", minId: "min-ram-avail", maxId: "max-ram-avail", avgId: "avg-ram-avail" },\n'
        printf '        ];\n'
        printf '        async function refresh() {\n'
        printf '            try {\n'
        printf '                const r = await fetch("system.json?ts=" + Date.now(), { cache: "no-store" });\n'
        printf '                if (!r.ok) return;\n'
        printf '                const d = await r.json();\n'
        printf '                if (!d || !d.generation) return;\n'
        printf '                for (const row of rows) {\n'
        printf '                    set(row.curId, fmt(d[row.cur]));\n'
        printf '                    set(row.minId, fmt(d[row.min]));\n'
        printf '                    set(row.maxId, fmt(d[row.max]));\n'
        printf '                    set(row.avgId, fmt(d[row.avg]));\n'
        printf '                }\n'
        printf '                if (d.cpu_count != null) set("info-cpu-count", d.cpu_count + " cores");\n'
        printf '                if (d.ram_total_gib != null) set("info-ram-total", fmt(d.ram_total_gib) + " GiB");\n'
        printf '                set("info-updated", d.timestamp ? new Date(d.timestamp).toLocaleTimeString() : "—");\n'
        printf '            } catch (e) { /* ignore network errors */ }\n'
        printf '        }\n'
        printf '        let refreshTimer = null;\n'
        printf '        function startAutoRefresh() {\n'
        printf '            if (refreshTimer === null) refreshTimer = window.setInterval(refresh, 5000);\n'
        printf '        }\n'
        printf '        function stopAutoRefresh() {\n'
        printf '            if (refreshTimer !== null) window.clearInterval(refreshTimer);\n'
        printf '            refreshTimer = null;\n'
        printf '        }\n'
        printf '        function resumeAutoRefresh() {\n'
        printf '            refresh();\n'
        printf '            window.setTimeout(refresh, 250);\n'
        printf '            startAutoRefresh();\n'
        printf '        }\n'
        printf '        function resumeFromNavigation(event) {\n'
        printf '            if (event && event.persisted) {\n'
        printf '                window.location.reload();\n'
        printf '                return;\n'
        printf '            }\n'
        printf '            resumeAutoRefresh();\n'
        printf '        }\n'
        printf '        window.addEventListener("pageshow", resumeFromNavigation);\n'
        printf '        document.addEventListener("visibilitychange", () => {\n'
        printf '            if (document.visibilityState === "visible") resumeAutoRefresh();\n'
        printf '            else stopAutoRefresh();\n'
        printf '        });\n'
        printf '        window.addEventListener("pagehide", stopAutoRefresh);\n'
        printf '        resumeAutoRefresh();\n'
        printf '    </script>\n'
    } >>"$index_file"
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

write_env_page() {
    local artifacts_root index_file page_title env_text

    artifacts_root=$(build_artifacts_root)
    write_html_favicon "$artifacts_root" || return 1
    index_file="$artifacts_root/env.html"
    page_title="GitTally [$(repository_simple_name)] - Env"
    mkdir -p "$artifacts_root" || return 1

    env_text=$(print_env)

    {
        printf '<!doctype html>\n'
        printf '<html lang="en">\n'
        printf '<head>\n'
        printf '    <meta charset="utf-8">\n'
        printf '    <meta name="viewport" content="width=device-width, initial-scale=1">\n'
        printf '    <meta http-equiv="Cache-Control" content="no-store">\n'
        printf '    <title>%s</title>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >"$index_file"
    write_html_favicon_links "$index_file"
    {
        printf '    <style>\n'
        printf '        :root { color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --row: #f9fafb; --link: #155eef; }\n'
        printf '        @media (prefers-color-scheme: dark) { :root { --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --row: #182235; --link: #93c5fd; } }\n'
        printf '        * { box-sizing: border-box; }\n'
        printf '        body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }\n'
        printf '        main { width: min(1180px, calc(100%% - 32px)); margin: 32px auto; }\n'
        printf '        h1 { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; font-size: 28px; font-weight: 700; }\n'
        printf '        h1 img { width: 32px; height: 32px; flex: none; }\n'
        printf '        .title-home { display: inline-flex; flex: none; }\n'
        printf '        a { color: var(--link); font-weight: 650; text-decoration: none; }\n'
        printf '        a:hover { text-decoration: underline; }\n'
        printf '        .view-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 18px; }\n'
        printf '        .view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }\n'
        printf '        .view-toggle a, .view-toggle span { display: inline-flex; min-width: 88px; justify-content: center; padding: 7px 12px; font-weight: 700; }\n'
        printf '        .view-toggle span { background: var(--link); color: white; }\n'
        printf '        .view-toggle a { color: var(--link); }\n'
        printf '        .view-toggle a:hover { background: color-mix(in srgb, var(--link) 8%%, transparent); text-decoration: none; }\n'
        printf '        .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 28px rgb(15 23 42 / 0.08); }\n'
        printf '        pre { margin: 0; padding: 16px 20px; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-all; }\n'
        printf '        .site-footer { width: min(1180px, calc(100%% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }\n'
        printf '        @media (max-width: 680px) { body { font-size: 14px; } main { margin: 16px auto; } h1 { font-size: 22px; } .view-toggle a, .view-toggle span { min-width: 0; padding: 6px 9px; font-size: 13px; } pre { font-size: 13px; padding: 12px; } }\n'
        printf '    </style>\n'
        printf '</head>\n'
        printf '<body>\n'
        printf '    <main>\n'
        printf '        <h1><a class="title-home" href="index.html" aria-label="Open latest builds"><img src="favicon.svg" alt="" aria-hidden="true"></a>%s</h1>\n' "$(printf '%s' "$page_title" | html_escape)"
    } >>"$index_file"
    write_build_artifact_view_toggle "$index_file" Env
    {
        printf '        <div class="table-wrap">\n'
        printf '            <pre>%s</pre>\n' "$(printf '%s' "$env_text" | html_escape)"
        printf '        </div>\n'
        printf '    </main>\n'
    } >>"$index_file"
    write_html_footer "$index_file"
    {
        printf '</body>\n'
        printf '</html>\n'
    } >>"$index_file"
}

start_resource_monitor() {
    [ -f /proc/stat ] && [ -f /proc/meminfo ] || return 0

    local artifacts_root pid_file old_pid
    artifacts_root=$(build_artifacts_root)
    pid_file="$artifacts_root/system_monitor.pid"
    if [ -f "$pid_file" ]; then
        old_pid=$(cat "$pid_file" 2>/dev/null)
        if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
            kill "$old_pid" 2>/dev/null || true
        fi
        rm -f "$pid_file"
    fi
    rm -f "$artifacts_root/system_state.dat" "$artifacts_root/system.json"

    (
        local cpu_count prev_total prev_idle
        cpu_count=$(nproc)
        read -r prev_total prev_idle < <(
            awk '/^cpu / {idle=$5; total=0; for(i=2;i<=NF;i++) total+=$i; print total, idle; exit}' /proc/stat
        )
        while true; do
            sleep 5
            local total idle total_diff idle_diff cpu_used cpu_idle
            local ram_total_gib ram_used_gib ram_free_gib timestamp
            local artifacts_root system_file state_file
            read -r total idle < <(
                awk '/^cpu / {idle=$5; total=0; for(i=2;i<=NF;i++) total+=$i; print total, idle; exit}' /proc/stat
            )
            read -r ram_total_gib ram_used_gib ram_free_gib < <(
                LC_ALL=C awk '/^MemTotal:/ {total=$2} /^MemAvailable:/ {avail=$2}
                     END { used=total-avail; printf "%.2f %.2f %.2f\n", total/1024/1024, used/1024/1024, avail/1024/1024 }' \
                     /proc/meminfo
            )
            total_diff=$((total - prev_total))
            idle_diff=$((idle - prev_idle))
            if [ "$total_diff" -gt 0 ]; then
                cpu_used=$(LC_ALL=C awk "BEGIN {printf \"%.2f\", $cpu_count * ($total_diff - $idle_diff) / $total_diff}")
                cpu_idle=$(LC_ALL=C awk "BEGIN {printf \"%.2f\", $cpu_count - $cpu_used}")
            else
                cpu_used="0.00"
                cpu_idle=$(printf "%.2f" "$cpu_count")
            fi
            prev_total=$total
            prev_idle=$idle
            artifacts_root=$(build_artifacts_root)
            system_file="$artifacts_root/system.json"
            state_file="$artifacts_root/system_state.dat"
            mkdir -p "$artifacts_root" || continue
            timestamp=$(date -Iseconds)
            LC_ALL=C awk -v ts="$timestamp" -v gen="$monitor_generation" -v cc="$cpu_count" -v rtg="$ram_total_gib" \
                -v cu="$cpu_used" -v ci="$cpu_idle" -v ru="$ram_used_gib" -v rf="$ram_free_gib" \
                -v sf="$state_file" \
            'BEGIN {
                n = 0
                cu_min = cu; cu_max = cu; cu_sum = 0
                ci_min = ci; ci_max = ci; ci_sum = 0
                ru_min = ru; ru_max = ru; ru_sum = 0
                rf_min = rf; rf_max = rf; rf_sum = 0
                while ((getline line < sf) > 0) {
                    nf = split(line, f, " ")
                    if (nf >= 13) {
                        n = f[1]+0
                        cu_min = f[2]+0; cu_max = f[3]+0; cu_sum = f[4]+0
                        ci_min = f[5]+0; ci_max = f[6]+0; ci_sum = f[7]+0
                        ru_min = f[8]+0; ru_max = f[9]+0; ru_sum = f[10]+0
                        rf_min = f[11]+0; rf_max = f[12]+0; rf_sum = f[13]+0
                    }
                }
                close(sf)
                n++
                if (n == 1 || cu < cu_min) cu_min = cu
                if (n == 1 || cu > cu_max) cu_max = cu
                cu_sum += cu
                if (n == 1 || ci < ci_min) ci_min = ci
                if (n == 1 || ci > ci_max) ci_max = ci
                ci_sum += ci
                if (n == 1 || ru < ru_min) ru_min = ru
                if (n == 1 || ru > ru_max) ru_max = ru
                ru_sum += ru
                if (n == 1 || rf < rf_min) rf_min = rf
                if (n == 1 || rf > rf_max) rf_max = rf
                rf_sum += rf
                printf "%d %.4f %.4f %.4f  %.4f %.4f %.4f  %.4f %.4f %.4f  %.4f %.4f %.4f\n",
                    n, cu_min, cu_max, cu_sum, ci_min, ci_max, ci_sum,
                    ru_min, ru_max, ru_sum, rf_min, rf_max, rf_sum > sf
                close(sf)
                printf "{\"timestamp\":\"%s\",\"generation\":\"%s\",\"cpu_count\":%d,\"sample_count\":%d,", ts, gen, cc, n
                printf "\"cpu_used\":%.2f,\"cpu_used_min\":%.2f,\"cpu_used_max\":%.2f,\"cpu_used_avg\":%.2f,", cu, cu_min, cu_max, cu_sum/n
                printf "\"cpu_idle\":%.2f,\"cpu_idle_min\":%.2f,\"cpu_idle_max\":%.2f,\"cpu_idle_avg\":%.2f,", ci, ci_min, ci_max, ci_sum/n
                printf "\"ram_total_gib\":%.2f,", rtg
                printf "\"ram_used_gib\":%.2f,\"ram_used_gib_min\":%.2f,\"ram_used_gib_max\":%.2f,\"ram_used_gib_avg\":%.2f,", ru, ru_min, ru_max, ru_sum/n
                printf "\"ram_free_gib\":%.2f,\"ram_free_gib_min\":%.2f,\"ram_free_gib_max\":%.2f,\"ram_free_gib_avg\":%.2f}\n", rf, rf_min, rf_max, rf_sum/n
            }' > "${system_file}.tmp" && mv "${system_file}.tmp" "${system_file}" && write_system_page || true
        done
    ) &
    echo $! > "$pid_file"
}

write_artifacts_root_index() {
    write_artifacts_root_index_page latest || return 1
    write_artifacts_root_index_page branches || return 1
    write_artifacts_root_index_page history || return 1
    if [ -z "${active_build_branch:-}" ]; then
        write_current_build_page
    fi
    write_system_page || return 1
}

detect_artifact_http_server_host() {
    local host

    if [ -n "$artifact_http_server_host" ]; then
        echo "$artifact_http_server_host"
        return 0
    fi

    if [ "$artifact_http_server_bind_address" != "0.0.0.0" ]; then
        echo "$artifact_http_server_bind_address"
        return 0
    fi

    if command -v ip >/dev/null 2>&1; then
        host=$(ip route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -n 1)
        if [ -n "$host" ]; then
            echo "$host"
            return 0
        fi
    fi

    if command -v hostname >/dev/null 2>&1; then
        host=$(hostname -I 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^[0-9.]+$/) { print $i; exit } }')
        if [ -n "$host" ]; then
            echo "$host"
            return 0
        fi
    fi

    echo "127.0.0.1"
}

default_artifact_public_base_url() {
    local port="$1"
    local url_host

    url_host=$(detect_artifact_http_server_host)
    echo "http://$url_host:$port/"
}

start_artifact_http_server_process() {
    local port="$1"
    local bind_address="$2"
    local directory="$3"
    local cancel_request_file="$4"
    local cancel_token_file="$5"
    local results_file="$6"

    GITTALLY_GITEA_BASE_URL="$gitea_base_url" \
        GITTALLY_GITEA_OWNER="$gitea_owner" \
        GITTALLY_GITEA_REPO="$gitea_repo" \
        GITTALLY_GITEA_TOKEN="$gitea_token" \
        GITTALLY_GITEA_STATUS_CONTEXT="$gitea_status_context" \
        GITTALLY_GITEA_DELETED_STATUS_DESCRIPTION="$(gitea_deleted_status_description)" \
        GITTALLY_SCRIPT_VERSION="$script_version" \
        GITTALLY_IMPRESSUM_URL="$impressum_url" \
        python3 -c '
import functools
import datetime
import hashlib
import html
import http.server
import json
import os
import re
import sys
import urllib.parse
import urllib.request

port = int(sys.argv[1])
bind_address = sys.argv[2]
directory = sys.argv[3]
cancel_request_file = sys.argv[4]
cancel_token_file = sys.argv[5]
results_file = sys.argv[6]
gitea_base_url = os.environ.get("GITTALLY_GITEA_BASE_URL", "")
gitea_owner = os.environ.get("GITTALLY_GITEA_OWNER", "")
gitea_repo = os.environ.get("GITTALLY_GITEA_REPO", "")
gitea_token = os.environ.get("GITTALLY_GITEA_TOKEN", "")
gitea_status_context = os.environ.get("GITTALLY_GITEA_STATUS_CONTEXT", "")
gitea_deleted_status_description = os.environ.get("GITTALLY_GITEA_DELETED_STATUS_DESCRIPTION", "Build status deleted")
script_version = os.environ.get("GITTALLY_SCRIPT_VERSION", "")
impressum_url = os.environ.get("GITTALLY_IMPRESSUM_URL", "")

class ArtifactRequestHandler(http.server.SimpleHTTPRequestHandler):
    extensions_map = {
        **http.server.SimpleHTTPRequestHandler.extensions_map,
        ".log": "text/plain; charset=utf-8",
        ".sh": "text/x-shellscript; charset=utf-8",
    }
    no_store_suffixes = (".html", ".json", ".log")

    def is_control_path(self, request_path, control_path):
        request_path = request_path.rstrip("/") or "/"
        return request_path == control_path or request_path.endswith(control_path)

    def end_headers(self):
        request_path = urllib.parse.urlparse(self.path).path
        if request_path.endswith(self.no_store_suffixes) or request_path.startswith("/control/"):
            self.send_header("Cache-Control", "no-store, max-age=0")
            self.send_header("Pragma", "no-cache")
            self.send_header("Expires", "0")
        super().end_headers()

    def do_GET(self):
        request = urllib.parse.urlparse(self.path)
        if self.is_control_path(request.path, "/control/status"):
            self.handle_status(urllib.parse.parse_qs(request.query))
            return
        if self.handle_artifact_index_request(request.path):
            return
        super().do_GET()

    def handle_artifact_index_request(self, request_path):
        request_path = urllib.parse.unquote(request_path)
        match = re.fullmatch(r"/?branches/([A-Za-z0-9._-]+)/index\.html", request_path)
        if not match:
            return False
        artifact_key = match.group(1)
        artifact_dir = os.path.join(directory, "branches", artifact_key)
        content = self.read_artifact_index_content(artifact_dir)
        if content is None:
            self.send_error(404)
            return True
        branch = self.branch_for_artifact_key(artifact_key)
        payload = self.render_artifact_index_page(branch, content).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)
        return True

    def read_artifact_index_content(self, artifact_dir):
        content_file = os.path.join(artifact_dir, "artifact-index-content.html")
        try:
            with open(content_file, encoding="utf-8") as content_input:
                return content_input.read()
        except FileNotFoundError:
            pass
        old_index_file = os.path.join(artifact_dir, "index.html")
        try:
            with open(old_index_file, encoding="utf-8") as index_input:
                old_index = index_input.read()
        except FileNotFoundError:
            return None
        article_match = re.search(r"<article\b[^>]*>.*?</article>", old_index, re.IGNORECASE | re.DOTALL)
        if article_match:
            return article_match.group(0)
        main_match = re.search(r"<main\b[^>]*>(.*?)</main>", old_index, re.IGNORECASE | re.DOTALL)
        if main_match:
            return re.sub(
                r"\s*<h1\b[^>]*>.*?</h1>\s*",
                "",
                main_match.group(1),
                count=1,
                flags=re.IGNORECASE | re.DOTALL,
            ).strip()
        return "<article><p>Could not extract artifact index content from the stored page.</p></article>"

    def branch_for_artifact_key(self, artifact_key):
        try:
            with open(results_file, encoding="utf-8") as results_input:
                for line in results_input:
                    fields = line.rstrip("\n").split("\t")
                    while len(fields) < 6:
                        fields.append("")
                    branch, _commit, _status, _timestamp, _duration, stored_artifact_key = fields[:6]
                    if stored_artifact_key == artifact_key:
                        return branch
        except FileNotFoundError:
            pass
        return artifact_key

    def render_artifact_index_page(self, branch, content):
        escaped_branch = html.escape(branch)
        escaped_branch_title = html.escape(branch, quote=True)
        branch_url = self.gitea_branch_web_url(branch)
        if branch_url:
            branch_title_html = f"""<span class="title-branch-wrap"><a class="title-branch" href="{html.escape(branch_url, quote=True)}" target="_blank" rel="noopener noreferrer" title="{escaped_branch_title}"><span class="title-branch-text">{escaped_branch}</span><span class="title-jump" aria-hidden="true">↗</span></a><button class="copy-button" type="button" data-copy="{escaped_branch_title}" title="Copy branch name" aria-label="Copy branch name">⧉</button></span>"""
        else:
            branch_title_html = f"""<span class="title-branch-wrap"><span class="title-branch" title="{escaped_branch_title}"><span class="title-branch-text">{escaped_branch}</span></span><button class="copy-button" type="button" data-copy="{escaped_branch_title}" title="Copy branch name" aria-label="Copy branch name">⧉</button></span>"""
        escaped_version = html.escape(script_version)
        escaped_impressum_url = html.escape(impressum_url, quote=True)
        return f"""<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Build artifacts: {escaped_branch}</title>
    <link rel="icon" href="../../favicon.svg" type="image/svg+xml">
    <link rel="shortcut icon" href="../../favicon.svg" type="image/svg+xml">
    <style>
        :root {{ color-scheme: light dark; --bg: #f6f8fa; --panel: #ffffff; --text: #1f2937; --muted: #6b7280; --border: #d7dde5; --link: #155eef; }}
        @media (prefers-color-scheme: dark) {{ :root {{ --bg: #111827; --panel: #1f2937; --text: #f3f4f6; --muted: #9ca3af; --border: #374151; --link: #93c5fd; }} }}
        * {{ box-sizing: border-box; }}
        body {{ margin: 0; background: var(--bg); color: var(--text); font: 14px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
        main {{ width: min(1180px, calc(100% - 32px)); margin: 32px auto; }}
        h1 {{ display: flex; align-items: center; gap: 10px; min-width: 0; margin: 0 0 18px; font-size: 28px; font-weight: 700; white-space: nowrap; }}
        h1 img {{ width: 32px; height: 32px; flex: none; }}
        .title-home {{ display: inline-flex; flex: none; }}
        .title-prefix {{ flex: none; }}
        .title-branch-wrap {{ display: inline-flex; align-items: center; gap: 5px; min-width: 0; }}
        .title-branch {{ display: inline-flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; white-space: nowrap; }}
        .title-branch-text {{ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
        .title-jump {{ flex: none; color: var(--muted); font-size: 0.72em; }}
        h2 {{ margin: 26px 0 12px; font-size: 18px; }}
        article {{ border: 1px solid var(--border); border-radius: 8px; background: var(--panel); padding: 22px; }}
        article ul {{ margin: 0 0 16px 20px; padding: 0; }}
        article li {{ margin: 0 0 7px; }}
        a {{ color: var(--link); font-weight: 650; text-decoration: none; }}
        a:hover {{ text-decoration: underline; }}
        h1 .title-branch {{ color: inherit; font-weight: inherit; text-decoration: none; }}
        h1 .title-branch:hover {{ color: var(--link); text-decoration: underline; }}
        h1 .title-branch:hover .title-jump {{ color: var(--link); }}
        .copy-button {{ appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--muted); font: 14px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; }}
        .copy-button:hover {{ border-color: var(--border); background: color-mix(in srgb, var(--link) 8%, transparent); color: var(--link); }}
        .copy-button.is-copied {{ color: #166534; }}
        .site-footer {{ width: min(1180px, calc(100% - 32px)); margin: 24px auto 32px; color: var(--muted); font-size: 12px; }}
    </style>
</head>
<body>
    <main>
        <h1><a class="title-home" href="../../index.html" aria-label="Open latest builds"><img src="../../favicon.svg" alt="" aria-hidden="true"></a><span class="title-prefix">Build artifacts:</span> {branch_title_html}</h1>
        {content}
    </main>
    <footer class="site-footer"><a href="../../about.html"><strong><em>gitTally v{escaped_version}</em></strong></a> - (c) <a href="https://michael.hoennig.de" target="_blank" rel="noopener noreferrer">Michael Hönnig</a>, 2026 - Licensed under the <a href="../../license.html">MIT License</a> - <a href="{escaped_impressum_url}" target="_blank" rel="noopener noreferrer">Impressum (Legal Disclosure)</a></footer>
    <script>
        async function copyToClipboard(button) {{
            const text = button.dataset.copy || "";
            if (!text) return;
            try {{
                if (navigator.clipboard && window.isSecureContext) {{
                    await navigator.clipboard.writeText(text);
                }} else {{
                    const input = document.createElement("textarea");
                    input.value = text;
                    input.style.position = "fixed";
                    input.style.left = "-9999px";
                    document.body.appendChild(input);
                    input.focus();
                    input.select();
                    document.execCommand("copy");
                    input.remove();
                }}
                button.classList.add("is-copied");
                window.setTimeout(() => button.classList.remove("is-copied"), 900);
            }} catch (error) {{
                return;
            }}
        }}
        document.addEventListener("click", (event) => {{
            const button = event.target.closest(".copy-button[data-copy]");
            if (!button) return;
            event.preventDefault();
            copyToClipboard(button);
        }});
    </script>
</body>
</html>
"""

    def gitea_branch_web_url(self, branch):
        if not (gitea_base_url and gitea_owner and gitea_repo):
            return ""
        return (
            gitea_base_url.rstrip("/") + "/" +
            urllib.parse.quote(gitea_owner, safe="") + "/" +
            urllib.parse.quote(gitea_repo, safe="") + "/src/branch/" +
            urllib.parse.quote(branch, safe="/")
        )

    def do_POST(self):
        request_path = urllib.parse.urlparse(self.path).path
        if self.is_control_path(request_path, "/control/cancel"):
            self.handle_cancel()
        elif self.is_control_path(request_path, "/control/restart"):
            body = self.read_form_body()
            if body is None:
                return
            self.handle_restart(urllib.parse.parse_qs(body))
        elif self.is_control_path(request_path, "/control/delete"):
            body = self.read_form_body()
            if body is None:
                return
            self.handle_delete(urllib.parse.parse_qs(body))
        else:
            self.send_error(404)
            return

    def read_form_body(self):
        content_length = int(self.headers.get("Content-Length", "0"))
        if content_length > 4096:
            self.send_error(413)
            return None
        return self.rfile.read(content_length).decode("utf-8")

    def handle_status(self, query):
        commit = query.get("commit", [""])[0]
        local_status = query.get("local_status", [""])[0]
        if not re.fullmatch(r"[0-9a-fA-F]{7,40}", commit):
            self.send_error(400)
            return
        if not re.fullmatch(r"[A-Za-z_-]+", local_status):
            self.send_error(400)
            return
        status = self.read_gitea_build_status(commit) or local_status
        payload = json.dumps({"status": status}).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def read_gitea_build_status(self, commit):
        if not (gitea_base_url and gitea_owner and gitea_repo and gitea_token and gitea_status_context):
            return None
        owner = urllib.parse.quote(gitea_owner, safe="")
        repo = urllib.parse.quote(gitea_repo, safe="")
        status_url = (
            gitea_base_url.rstrip("/") + "/api/v1/repos/" + owner + "/" + repo +
            "/commits/" + commit + "/statuses?sort=recentupdate"
        )
        request = urllib.request.Request(
            status_url,
            headers={"Authorization": "token " + gitea_token},
            method="GET",
        )
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                if response.status < 200 or response.status >= 300:
                    return None
                statuses = json.loads(response.read().decode("utf-8"))
        except Exception:
            return None
        for status in statuses:
            if status.get("context") != gitea_status_context:
                continue
            if status.get("description") == gitea_deleted_status_description:
                return None
            state = status.get("state", "")
            if state == "success":
                return "success"
            if state in ("failure", "error", "warning"):
                return "failed"
            if state == "pending":
                return "running"
            return None
        return None

    def handle_cancel(self):
        body = self.read_form_body()
        if body is None:
            return
        submitted_token = urllib.parse.parse_qs(body).get("token", [""])[0]
        try:
            with open(cancel_token_file, encoding="utf-8") as token_input:
                expected_token = token_input.readline().strip()
        except FileNotFoundError:
            expected_token = ""
        if not expected_token or submitted_token != expected_token:
            self.send_error(403)
            return
        with open(cancel_request_file, "w", encoding="utf-8") as request_output:
            request_output.write("cancel\n")
        self.send_response(202)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(b"Cancellation requested.\n")

    def handle_restart(self, form):
        branch = form.get("branch", [""])[0]
        commit = form.get("commit", [""])[0]
        return_to = form.get("return_to", ["index.html"])[0]
        if not branch or "\n" in branch or "\r" in branch:
            self.send_error(400)
            return
        if not commit:
            commit = "0000000"
        elif not re.fullmatch(r"[0-9a-fA-F]{7,40}", commit):
            self.send_error(400)
            return
        if return_to not in ("index.html", "branches.html"):
            return_to = "index.html"
        timestamp = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        branch_key = re.sub(r"[^A-Za-z0-9._-]", "_", branch)
        branch_hash = hashlib.sha256(branch.encode("utf-8")).hexdigest()[:12]
        timestamp_key = re.sub(r"[^A-Za-z0-9._-]", "_", timestamp)
        artifact_key = branch_key + "-" + branch_hash + "-restart-" + timestamp_key
        os.makedirs(os.path.dirname(results_file), exist_ok=True)
        with open(results_file, "a", encoding="utf-8") as results_output:
            results_output.write("\t".join([branch, commit, "pending", timestamp, "", artifact_key]) + "\n")
        self.mark_latest_page_pending(branch)
        self.send_response(303)
        self.send_header("Location", "/" + return_to)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(b"Restart requested.\n")

    def handle_delete(self, form):
        branch = form.get("branch", [""])[0]
        commit = form.get("commit", [""])[0]
        artifact_key = form.get("artifact_key", [""])[0]
        return_to = form.get("return_to", ["index.html"])[0]
        if not branch or "\n" in branch or "\r" in branch:
            self.send_error(400)
            return
        if not re.fullmatch(r"[0-9a-fA-F]{7,40}", commit):
            self.send_error(400)
            return
        if not re.fullmatch(r"[A-Za-z0-9._-]+", artifact_key):
            self.send_error(400)
            return
        if return_to not in ("index.html", "branches.html", "history.html"):
            return_to = "index.html"
        if self.delete_stored_status(branch, commit, artifact_key):
            self.publish_deleted_gitea_status(commit)
            self.remove_visible_status(branch, commit, artifact_key)
        self.send_response(303)
        self.send_header("Location", "/" + return_to)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(b"Stored status deleted.\n")

    def delete_stored_status(self, wanted_branch, wanted_commit, wanted_artifact_key):
        removed = False
        kept_lines = []
        try:
            with open(results_file, encoding="utf-8") as results_input:
                lines = results_input.readlines()
        except FileNotFoundError:
            return False
        for line in lines:
            fields = line.rstrip("\n").split("\t")
            while len(fields) < 6:
                fields.append("")
            branch, commit, status, timestamp, duration, artifact_key = fields[:6]
            if branch == wanted_branch and commit == wanted_commit and artifact_key == wanted_artifact_key:
                removed = True
                continue
            kept_lines.append(line)
        if removed:
            os.makedirs(os.path.dirname(results_file), exist_ok=True)
            temp_file = results_file + ".delete"
            with open(temp_file, "w", encoding="utf-8") as results_output:
                results_output.writelines(kept_lines)
            os.replace(temp_file, results_file)
        return removed

    def publish_deleted_gitea_status(self, commit):
        if not (gitea_base_url and gitea_owner and gitea_repo and gitea_token and gitea_status_context):
            return
        payload = json.dumps({
            "state": "warning",
            "context": gitea_status_context,
            "description": gitea_deleted_status_description,
        }).encode("utf-8")
        owner = urllib.parse.quote(gitea_owner, safe="")
        repo = urllib.parse.quote(gitea_repo, safe="")
        status_url = gitea_base_url.rstrip("/") + "/api/v1/repos/" + owner + "/" + repo + "/statuses/" + commit
        request = urllib.request.Request(
            status_url,
            data=payload,
            headers={
                "Authorization": "token " + gitea_token,
                "Content-Type": "application/json",
            },
            method="POST",
        )
        try:
            urllib.request.urlopen(request, timeout=10).close()
        except Exception:
            print("WARNING: could not publish deleted Gitea build status for " + commit + ".", file=sys.stderr)

    def remove_visible_status(self, branch, commit, artifact_key):
        escaped_artifact_key = html.escape(artifact_key, quote=True)
        row_pattern = re.compile(
            r"\s*<tr\b[^>]*\bdata-artifact-key=\"" + re.escape(escaped_artifact_key) +
            r"\"[^>]*>.*?</tr>\n?",
            re.DOTALL,
        )
        branch_row = (
            "                    <tr class=\"status-unknown\" data-branch=\"" + html.escape(branch, quote=True) +
            "\" data-artifact-key=\"\" data-commit=\"" + html.escape(commit, quote=True) +
            "\" data-local-status=\"unknown\"><td><span class=\"status status-unknown\">unknown</span></td>" +
            "<td class=\"branch\">" + html.escape(branch) + "</td><td><code>" + html.escape(commit[:12]) +
            "</code></td><td></td><td></td><td></td><td>n/a</td><td class=\"actions-cell\"><div class=\"actions\">" +
            "<form class=\"action-form\" method=\"post\" action=\"/control/restart\"><input type=\"hidden\" name=\"branch\" value=\"" +
            html.escape(branch, quote=True) + "\"><input type=\"hidden\" name=\"commit\" value=\"" +
            html.escape(commit, quote=True) + "\"><input type=\"hidden\" name=\"return_to\" value=\"branches.html\"><button class=\"action-button\" type=\"submit\" title=\"Restart build\" aria-label=\"Restart build\">↻</button></form>" +
            "</div></td></tr>\n"
        )
        for page_name in ("index.html", "branches.html", "history.html"):
            page_file = os.path.join(directory, page_name)
            try:
                with open(page_file, encoding="utf-8") as page_input:
                    page_html = page_input.read()
            except FileNotFoundError:
                continue
            if page_name == "branches.html":
                page_html = row_pattern.sub(branch_row, page_html, count=1)
            else:
                page_html = row_pattern.sub("", page_html, count=1)
            page_html = re.sub(
                r"(<tbody>\n)\s*(</tbody>)",
                r"""\1                    <tr><td class="empty" colspan="8">No builds archived yet.</td></tr>\n                \2""",
                page_html,
            )
            with open(page_file, "w", encoding="utf-8") as page_output:
                page_output.write(page_html)

    def mark_latest_page_pending(self, branch):
        escaped_branch = html.escape(branch, quote=True)
        row_pattern = re.compile(
            r"(<tr class=\")status-[^\"]*(\" data-branch=\"" + re.escape(escaped_branch) +
            r"\" data-artifact-key=\"[^\"]*\"><td><span class=\"status )status-[^\"]*(\">)[^<]*(</span>)"
        )
        for page_name in ("index.html", "branches.html"):
            page_file = os.path.join(directory, page_name)
            try:
                with open(page_file, encoding="utf-8") as page_input:
                    page_html = page_input.read()
            except FileNotFoundError:
                continue
            page_html = row_pattern.sub(r"\1status-pending\2status-pending\3pending\4", page_html, count=1)
            with open(page_file, "w", encoding="utf-8") as page_output:
                page_output.write(page_html)

handler_class = functools.partial(ArtifactRequestHandler, directory=directory)
server = http.server.ThreadingHTTPServer((bind_address, port), handler_class)
try:
    server.serve_forever()
finally:
    server.server_close()
' "$port" "$bind_address" "$directory" "$cancel_request_file" "$cancel_token_file" "$results_file"
}

start_artifact_http_server() {
    local artifacts_root
    local results_file
    local port
    local max_port
    local url_host
    local public_base_url

    if [ "$use_artifact_http_server" != true ]; then
        return 0
    fi

    if ! command -v python3 >/dev/null 2>&1; then
        echo "WARNING: cannot start artifact HTTP server because python3 is not in PATH." >&2
        return 0
    fi

    if ! [[ "$artifact_http_server_port" =~ ^[0-9]+$ ]] ||
        [ "$artifact_http_server_port" -lt 1 ] ||
        [ "$artifact_http_server_port" -gt 65535 ]; then
        echo "WARNING: invalid GITTALLY_ARTIFACT_SERVER_PORT: $artifact_http_server_port" >&2
        return 0
    fi

    artifacts_root=$(build_artifacts_root)
    results_file=$(build_results_file)
    write_artifacts_root_index || {
        echo "WARNING: could not write artifact index." >&2
        return 0
    }

    port=$artifact_http_server_port
    max_port=$((artifact_http_server_port + 20))
    if [ "$max_port" -gt 65535 ]; then
        max_port=65535
    fi

    while [ "$port" -le "$max_port" ]; do
        start_artifact_http_server_process "$port" "$artifact_http_server_bind_address" "$artifacts_root" "$(build_cancel_request_file)" "$(build_cancel_token_file)" "$results_file" >/dev/null 2>&1 &
        artifact_http_server_pid=$!
        sleep 0.2
        if kill -0 "$artifact_http_server_pid" >/dev/null 2>&1; then
            url_host=$(detect_artifact_http_server_host)
            public_base_url="${artifact_public_base_url:-$(default_artifact_public_base_url "$port")}"
            artifact_http_server_local_url="http://$url_host:$port/"
            artifact_http_server_url=$(normalize_base_url "$public_base_url")
            echo "Artifact HTTP server: $artifact_http_server_local_url"
            echo "Artifact public URL: $artifact_http_server_url"
            echo "Artifact HTTP server bind address: $artifact_http_server_bind_address"
            echo "Artifact directory: file://$artifacts_root"
            return 0
        fi
        wait "$artifact_http_server_pid" 2>/dev/null || true
        artifact_http_server_pid=
        port=$((port + 1))
    done

    echo "WARNING: could not start artifact HTTP server on ${artifact_http_server_bind_address}:${artifact_http_server_port}-${max_port}." >&2
}

open_artifact_frontend_if_requested() {
    if [ "$open_artifact_frontend" != true ]; then
        return 0
    fi

    if [ -z "$artifact_http_server_local_url" ]; then
        echo "WARNING: cannot open artifact frontend because the HTTP server is not running." >&2
        return 0
    fi

    open_in_local_browser "artifact frontend" "$artifact_http_server_local_url"
}

artifact_nginx_write_ssl_options() {
    local certbot_conf="$1"

    mkdir -p "$certbot_conf" || return 1
    cat >"$certbot_conf/options-ssl-nginx.conf" <<'EOF'
ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
EOF

    if [ ! -f "$certbot_conf/ssl-dhparams.pem" ]; then
        if command -v curl >/dev/null 2>&1; then
            curl -fsSL -o "$certbot_conf/ssl-dhparams.pem" \
                https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem || return 1
        else
            echo "WARNING: cannot prepare nginx SSL parameters because curl is not in PATH." >&2
            return 1
        fi
    fi
    chmod 644 "$certbot_conf/options-ssl-nginx.conf" "$certbot_conf/ssl-dhparams.pem"
}

artifact_nginx_write_config() {
    local config_file="$1"
    local mode="$2"
    local auth_proxy_url="http://$artifact_auth_container_name:$artifact_auth_http_port"

    if [ "$mode" = init ]; then
        cat >"$config_file" <<EOF
events {}

http {
    server {
        listen 80;
        server_name $artifact_nginx_server_name;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://\$host\$request_uri;
        }
    }
}
EOF
        return 0
    fi

    {
        cat <<EOF
events {}

http {
    server {
        listen 80;
        server_name $artifact_nginx_server_name;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://\$host\$request_uri;
        }
    }

    server {
        listen 443 ssl;
        server_name $artifact_nginx_server_name;

        ssl_certificate /etc/letsencrypt/live/$artifact_nginx_server_name/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/$artifact_nginx_server_name/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }
EOF
        if artifact_auth_enabled; then
            cat <<EOF

        location = /oauth2/auth {
            internal;
            proxy_pass $auth_proxy_url/oauth2/auth;
            proxy_pass_request_body off;
            proxy_set_header Content-Length "";
            proxy_set_header Host \$host;
            proxy_set_header X-Original-URI \$request_uri;
            proxy_set_header X-Real-IP \$remote_addr;
            proxy_set_header X-Scheme \$scheme;
        }

        location /oauth2/ {
            proxy_pass $auth_proxy_url;
            proxy_set_header Host \$host;
            proxy_set_header X-Real-IP \$remote_addr;
            proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto \$scheme;
        }
EOF
        fi
        cat <<EOF

        location / {
EOF
        if artifact_auth_enabled; then
            cat <<'EOF'
            auth_request /oauth2/auth;
            error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri;
            auth_request_set $auth_user $upstream_http_x_auth_request_user;
            auth_request_set $auth_email $upstream_http_x_auth_request_email;
            proxy_set_header X-Auth-Request-User $auth_user;
            proxy_set_header X-Auth-Request-Email $auth_email;
EOF
        fi
        cat <<EOF
            proxy_pass http://$artifact_nginx_upstream_host:$artifact_http_server_port;
            proxy_set_header Host \$host;
            proxy_set_header X-Real-IP \$remote_addr;
            proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto \$scheme;
            add_header Cache-Control "no-store, max-age=0" always;
            add_header Pragma "no-cache" always;
            add_header Expires "0" always;
        }
    }
}
EOF
    } >"$config_file"
}

remove_docker_container_by_name() {
    local container_name="$1"

    if [ -n "$container_name" ]; then
        docker rm -f "$container_name" >/dev/null 2>&1 || true
    fi
}

remove_gittally_containers_by_label() {
    local role="$1"
    local container_ids

    container_ids=$(docker ps -aq \
        --filter "label=org.hostsharing.gittally=true" \
        --filter "label=org.hostsharing.gittally.repository=$(repository_key)" \
        --filter "label=org.hostsharing.gittally.role=$role" 2>/dev/null || true)
    if [ -n "$container_ids" ]; then
        docker rm -f $container_ids >/dev/null 2>&1 || true
    fi
}

cleanup_stale_build_runtime() {
    if ! command -v docker >/dev/null 2>&1; then
        return 0
    fi

    remove_docker_container_by_name "$(docker_build_container_name)"
    remove_gittally_containers_by_label build
}

container_ports_include_host_port() {
    local ports="$1"
    local port="$2"

    [[ "$ports" == *":$port->"* ]]
}

remove_gittally_port_containers() {
    local port
    local container_id
    local container_name
    local container_ports
    local container_labels

    for port in "$@"; do
        while IFS=$'\t' read -r container_id container_name container_ports container_labels; do
            if [ -z "$container_id" ]; then
                continue
            fi
            if ! container_ports_include_host_port "$container_ports" "$port"; then
                continue
            fi
            if [[ "$container_labels" == *"org.hostsharing.gittally=true"* ]] ||
                [[ "$container_name" == gittally-* ]] ||
                [[ "$container_name" == git-watch-origin-and-test-nginx-* ]]; then
                echo "Removing stale GitTally container using port $port: $container_name"
                docker rm -f "$container_id" >/dev/null 2>&1 || true
            fi
        done < <(docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}\t{{.Labels}}' 2>/dev/null || true)
    done
}

warn_remaining_port_owners() {
    local port
    local container_id
    local container_name
    local container_ports
    local container_labels
    local has_owner=false

    for port in "$@"; do
        while IFS=$'\t' read -r container_id container_name container_ports container_labels; do
            if [ -z "$container_id" ]; then
                continue
            fi
            if container_ports_include_host_port "$container_ports" "$port"; then
                has_owner=true
                echo "WARNING: artifact nginx port $port is already used by Docker container $container_name ($container_id)." >&2
            fi
        done < <(docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}\t{{.Labels}}' 2>/dev/null || true)
    done

    if [ "$has_owner" = true ]; then
        echo "WARNING: remaining containers still use artifact nginx ports." >&2
        return 1
    fi
    return 0
}

artifact_nginx_ports_free() {
    local port
    local container_id
    local container_name
    local container_ports
    local container_labels

    for port in "$@"; do
        while IFS=$'\t' read -r container_id container_name container_ports container_labels; do
            if [ -n "$container_id" ] && container_ports_include_host_port "$container_ports" "$port"; then
                return 1
            fi
        done < <(docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}\t{{.Labels}}' 2>/dev/null || true)
    done
    return 0
}

wait_for_artifact_nginx_ports() {
    local attempt

    if ! warn_remaining_port_owners "$artifact_nginx_http_port" "$artifact_nginx_https_port"; then
        sleep 1
    fi
    for attempt in 1 2 3 4; do
        if artifact_nginx_ports_free "$artifact_nginx_http_port" "$artifact_nginx_https_port"; then
            return 0
        fi
        sleep 1
    done
    artifact_nginx_ports_free "$artifact_nginx_http_port" "$artifact_nginx_https_port"
}

cleanup_stale_artifact_nginx_containers() {
    remove_docker_container_by_name "$artifact_nginx_container_name"
    remove_docker_container_by_name "$artifact_auth_container_name"
    remove_gittally_containers_by_label nginx
    remove_gittally_containers_by_label auth
    remove_gittally_port_containers "$artifact_nginx_http_port" "$artifact_nginx_https_port"
    wait_for_artifact_nginx_ports
}

artifact_nginx_run_container() {
    local config_file="$1"
    local certbot_conf="$2"
    local certbot_www="$3"
    local nginx_log="$4"
    local container_id
    local -a docker_args
    local -a docker_label_args

    remove_docker_container_by_name "$artifact_nginx_container_name"
    artifact_nginx_container_started=false
    artifact_nginx_container_id=

    docker_args=(run -d --name "$artifact_nginx_container_name" \
        --publish "$artifact_nginx_http_port:80" \
        --publish "$artifact_nginx_https_port:443" \
        --network bridge \
        -v "$certbot_conf:/etc/letsencrypt" \
        -v "$certbot_www:/var/www/certbot" \
        -v "$nginx_log:/var/log/nginx" \
        -v "$config_file:/etc/nginx/nginx.conf:ro")
    mapfile -t docker_label_args < <(gittally_docker_label_args nginx)
    docker_args+=("${docker_label_args[@]}")

    if artifact_auth_enabled; then
        docker_args+=(--link "$artifact_auth_container_name:$artifact_auth_container_name")
    fi

    container_id=$(docker "${docker_args[@]}" nginx) || return 1
    artifact_nginx_container_id="$container_id"
    artifact_nginx_container_started=true
}

start_artifact_auth_proxy() {
    local public_base_url
    local redirect_url
    local gitea_base_url
    local container_id
    local -a docker_args
    local -a docker_label_args
    local -a email_domain_args=()
    local -a email_domains=()
    local -a cookie_domain_args=()
    local email_domain

    if ! artifact_auth_enabled; then
        return 0
    fi

    if [ -z "$artifact_auth_client_id" ] ||
        [ -z "$artifact_auth_client_secret" ] ||
        [ -z "$artifact_auth_cookie_secret" ]; then
        echo "WARNING: cannot enable artifact website Gitea login because OAuth2 client id, client secret, or cookie secret is empty." >&2
        return 1
    fi

    if ! [[ "$artifact_auth_http_port" =~ ^[0-9]+$ ]] ||
        [ "$artifact_auth_http_port" -lt 1 ] ||
        [ "$artifact_auth_http_port" -gt 65535 ]; then
        echo "WARNING: invalid GITTALLY_ARTIFACT_AUTH_HTTP_PORT: $artifact_auth_http_port" >&2
        return 1
    fi

    public_base_url=$(normalize_base_url "${artifact_public_base_url:-https://$artifact_nginx_server_name/}")
    redirect_url="${public_base_url}oauth2/callback"
    gitea_base_url="${artifact_auth_gitea_base_url%/}"

    IFS=',' read -r -a email_domains <<<"$artifact_auth_email_domains"
    for email_domain in "${email_domains[@]}"; do
        if [ -n "$email_domain" ]; then
            email_domain_args+=(--email-domain "$email_domain")
        fi
    done
    if [ "${#email_domain_args[@]}" -eq 0 ]; then
        email_domain_args=(--email-domain '*')
    fi

    if [ -n "$artifact_auth_cookie_domain" ]; then
        cookie_domain_args=(--cookie-domain "$artifact_auth_cookie_domain")
    fi

    remove_docker_container_by_name "$artifact_auth_container_name"
    artifact_auth_container_started=false
    artifact_auth_container_id=
    docker_args=(run -d --name "$artifact_auth_container_name" --network bridge)
    mapfile -t docker_label_args < <(gittally_docker_label_args auth)
    docker_args+=("${docker_label_args[@]}" "$artifact_auth_image"
        --http-address=0.0.0.0:"$artifact_auth_http_port"
        --provider=github
        --provider-display-name=Gitea
        --client-id="$artifact_auth_client_id"
        --client-secret="$artifact_auth_client_secret"
        --cookie-secret="$artifact_auth_cookie_secret"
        --cookie-secure=true
        --redirect-url="$redirect_url"
        --login-url="$gitea_base_url/login/oauth/authorize"
        --redeem-url="$gitea_base_url/login/oauth/access_token"
        --validate-url="$gitea_base_url/api/v1/user/emails"
        --reverse-proxy=true
        --set-xauthrequest=true
        --skip-provider-button=true
        "${email_domain_args[@]}"
        "${cookie_domain_args[@]}")

    if ! container_id=$(docker "${docker_args[@]}"); then
        echo "WARNING: could not start artifact website Gitea auth proxy $artifact_auth_container_name." >&2
        return 1
    fi
    artifact_auth_container_id="$container_id"
    artifact_auth_container_started=true

    echo "Artifact website auth: Gitea login via $gitea_base_url"
    echo "Artifact website auth callback: $redirect_url"
}

artifact_nginx_obtain_or_renew_certificate() {
    local certbot_conf="$1"
    local certbot_www="$2"
    local certbot_log="$3"
    local cert_file="$certbot_conf/live/$artifact_nginx_server_name/fullchain.pem"
    local -a certbot_extra_args=()
    local -a email_args=()

    if [ -n "$artifact_certbot_env" ]; then
        read -r -a certbot_extra_args <<<"$artifact_certbot_env"
    fi

    if [ -n "$artifact_letsencrypt_email" ]; then
        email_args=(--email "$artifact_letsencrypt_email")
    else
        email_args=(--register-unsafely-without-email)
    fi

    if [ -f "$cert_file" ]; then
        docker run --rm \
            -v "$certbot_conf:/etc/letsencrypt" \
            -v "$certbot_www:/var/www/certbot" \
            -v "$certbot_log:/var/log/letsencrypt" \
            certbot/certbot renew -q "${certbot_extra_args[@]}"
        return $?
    fi

    docker run --rm \
        -v "$certbot_conf:/etc/letsencrypt" \
        -v "$certbot_www:/var/www/certbot" \
        -v "$certbot_log:/var/log/letsencrypt" \
        certbot/certbot \
        certonly --webroot --webroot-path /var/www/certbot --cert-name "$artifact_nginx_server_name" \
        -d "$artifact_nginx_server_name" --rsa-key-size 4096 \
        --non-interactive --agree-tos "${email_args[@]}" "${certbot_extra_args[@]}"
}

start_artifact_nginx() {
    local certbot_conf
    local certbot_www
    local certbot_log
    local nginx_log
    local config_file
    local cert_file

    if [ "$use_artifact_nginx" != true ]; then
        return 0
    fi

    if ! command -v docker >/dev/null 2>&1; then
        echo "WARNING: cannot start artifact nginx because docker is not in PATH." >&2
        return 0
    fi

    if [ -z "$artifact_nginx_server_name" ]; then
        echo "WARNING: cannot start artifact nginx because GITTALLY_ARTIFACT_NGINX_SERVER_NAME is empty." >&2
        return 0
    fi

    if ! [[ "$artifact_nginx_http_port" =~ ^[0-9]+$ ]] ||
        ! [[ "$artifact_nginx_https_port" =~ ^[0-9]+$ ]] ||
        [ "$artifact_nginx_http_port" -lt 1 ] || [ "$artifact_nginx_http_port" -gt 65535 ] ||
        [ "$artifact_nginx_https_port" -lt 1 ] || [ "$artifact_nginx_https_port" -gt 65535 ]; then
        echo "WARNING: cannot start artifact nginx because nginx ports are invalid." >&2
        return 0
    fi

    if [ -z "$artifact_http_server_url" ]; then
        echo "WARNING: cannot start artifact nginx because artifact HTTP server is not running." >&2
        return 0
    fi

    if ! cleanup_stale_artifact_nginx_containers; then
        echo "WARNING: artifact nginx was not started because a configured port is still in use." >&2
        return 0
    fi

    certbot_conf="$artifact_nginx_state_dir/certbot/conf"
    certbot_www="$artifact_nginx_state_dir/certbot/www"
    certbot_log="$artifact_nginx_state_dir/certbot/log"
    nginx_log="$artifact_nginx_state_dir/nginx/log"
    config_file="$artifact_nginx_state_dir/nginx/nginx.conf"
    cert_file="$certbot_conf/live/$artifact_nginx_server_name/fullchain.pem"

    mkdir -p "$certbot_www" "$certbot_log" "$nginx_log" "$(dirname "$config_file")" || {
        echo "WARNING: could not prepare artifact nginx state directory: $artifact_nginx_state_dir" >&2
        return 0
    }
    chmod 755 "$certbot_www" "$certbot_log" "$nginx_log"

    artifact_nginx_write_ssl_options "$certbot_conf" || {
        echo "WARNING: could not prepare artifact nginx SSL options." >&2
        return 0
    }

    if [ -f "$cert_file" ]; then
        if ! start_artifact_auth_proxy; then
            echo "WARNING: artifact nginx HTTPS proxy was not started because artifact website auth setup failed." >&2
            return 0
        fi
        artifact_nginx_write_config "$config_file" full || return 0
    else
        artifact_nginx_write_config "$config_file" init || return 0
    fi

    if ! artifact_nginx_run_container "$config_file" "$certbot_conf" "$certbot_www" "$nginx_log"; then
        echo "WARNING: could not start artifact nginx container $artifact_nginx_container_name." >&2
        return 0
    fi

    if ! artifact_nginx_obtain_or_renew_certificate "$certbot_conf" "$certbot_www" "$certbot_log"; then
        echo "WARNING: artifact nginx is running, but Let's Encrypt certificate setup failed." >&2
        return 0
    fi

    if ! start_artifact_auth_proxy; then
        echo "WARNING: artifact nginx HTTPS proxy was not started because artifact website auth setup failed." >&2
        return 0
    fi

    artifact_nginx_write_config "$config_file" full || return 0
    if ! artifact_nginx_run_container "$config_file" "$certbot_conf" "$certbot_www" "$nginx_log"; then
        echo "WARNING: certificate is available, but restarting artifact nginx with HTTPS failed." >&2
        return 0
    fi

    echo "Artifact nginx proxy: http://$artifact_nginx_server_name:$artifact_nginx_http_port/ -> https://$artifact_nginx_server_name:$artifact_nginx_https_port/"
    echo "Artifact nginx public URL: $(normalize_base_url "${artifact_public_base_url:-https://$artifact_nginx_server_name/}")"
    echo "Artifact nginx upstream: http://$artifact_nginx_upstream_host:$artifact_http_server_port/"
    echo "Artifact nginx state: $artifact_nginx_state_dir"
}

is_below_known_report_index() {
    local report_dir="$1"
    shift
    local known_report_dir

    for known_report_dir in "$@"; do
        if [ "$known_report_dir" = "." ]; then
            return 0
        fi
        if [[ "$report_dir" == "$known_report_dir"/* ]]; then
            return 0
        fi
    done
    return 1
}

archived_artefact_dir_path() {
    local report_dir="$1"

    if [ "$report_dir" = build/reports ]; then
        echo reports
    else
        echo "reports/$report_dir"
    fi
}

write_archived_artefact_dir_links() {
    local index_file="$1"
    local artifact_tmp_dir="$2"
    local report_dir
    local archived_path
    local has_artefact_dirs=false
    local IFS=';'

    for report_dir in $build_artefact_dirs; do
        if [ -z "$report_dir" ]; then
            continue
        fi
        archived_path=$(archived_artefact_dir_path "$report_dir")
        if [ ! -d "$artifact_tmp_dir/$archived_path" ]; then
            continue
        fi

        has_artefact_dirs=true
        write_html_link "$index_file" "$archived_path/" "$report_dir"
    done

    [ "$has_artefact_dirs" = true ]
}

write_artifact_index() {
    local branch="$1"
    local artifact_tmp_dir="$2"
    local effective_build_command="$3"
    local index_file
    local relative_index
    local report_dir
    local -a report_index_dirs=()

    index_file=$(build_artifact_index_content_file "$artifact_tmp_dir")
    {
        printf '        <article>\n'
        printf '            <h2>Logs</h2>\n'
        printf '            <ul>\n'
    } >"$index_file"

    printf '        <li>Build command:<br/><code>%s</code></li>\n' \
            "$(printf '%s' "$effective_build_command" | html_escape)" \
            >>"$index_file"
        if [ -f "$artifact_tmp_dir/$build_stdout_log" ]; then
        write_html_link "$index_file" "$build_stdout_log" "Build stdout"
    elif [ -f "$artifact_tmp_dir/gradle.stdout.log" ]; then
        write_html_link "$index_file" "gradle.stdout.log" "Build stdout"
    fi
    if [ -f "$artifact_tmp_dir/$build_stderr_log" ]; then
        write_html_link "$index_file" "$build_stderr_log" "Build stderr"
    elif [ -f "$artifact_tmp_dir/gradle.stderr.log" ]; then
        write_html_link "$index_file" "gradle.stderr.log" "Build stderr"
    fi

    {
        printf '            </ul>\n'
        printf '            <h2>Build Artifacts</h2>\n'
        printf '            <ul>\n'
    } >>"$index_file"

    if [ -d "$artifact_tmp_dir/reports" ]; then
        write_archived_artefact_dir_links "$index_file" "$artifact_tmp_dir" || \
            write_html_link "$index_file" "reports/" "Archived artifact directories"
        while IFS= read -r relative_index; do
            report_dir=$(dirname "$relative_index")
            if is_below_known_report_index "$report_dir" "${report_index_dirs[@]}"; then
                continue
            fi
            report_index_dirs+=("$report_dir")
            write_html_link "$index_file" "reports/$relative_index" "reports/$relative_index"
        done < <(find "$artifact_tmp_dir/reports" -type f -name index.html -printf '%P\n' | awk '{ path=$0; depth=gsub("/", "/", path); print depth, length($0), $0 }' | sort -n -s | cut -d' ' -f3-)
    else
        printf '                <li>No artifact directories were produced by this build.</li>\n' >>"$index_file"
    fi

    {
        printf '            </ul>\n'
        printf '        </article>\n'
    } >>"$index_file"
}

copy_build_artefact_dirs() {
    local artifact_tmp_dir="$1"
    local report_dir
    local report_target
    local IFS=';'

    for report_dir in $build_artefact_dirs; do
        if [ -z "$report_dir" ] || [ ! -d "$report_dir" ]; then
            continue
        fi
        mkdir -p "$artifact_tmp_dir/reports" || return 1
        if [ "$report_dir" = build/reports ]; then
            cp -a "$report_dir/." "$artifact_tmp_dir/reports/" || return 1
        else
            report_target="$artifact_tmp_dir/$(archived_artefact_dir_path "$report_dir")"
            mkdir -p "$(dirname "$report_target")" || return 1
            cp -a "$report_dir" "$report_target" || return 1
        fi
    done
}

persist_build_artifacts() {
    local branch="$1"
    local stdout_file="$2"
    local stderr_file="$3"
    local artifact_key="$4"
    local effective_build_command="$5"
    local artifact_dir
    local artifact_tmp_dir

    artifact_dir=$(build_artifact_dir "$branch" "$artifact_key")
    artifact_tmp_dir="$artifact_dir.tmp.$$"

    rm -rf -- "$artifact_tmp_dir" || return 1
    mkdir -p "$artifact_tmp_dir" || return 1

    cp "$stdout_file" "$artifact_tmp_dir/$build_stdout_log" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    cp "$stderr_file" "$artifact_tmp_dir/$build_stderr_log" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    copy_build_artefact_dirs "$artifact_tmp_dir" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    write_html_favicon "$(build_artifacts_root)" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    write_script_download "$(build_artifacts_root)" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    write_html_about_page "$(build_artifacts_root)" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    write_html_license_page "$(build_artifacts_root)" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    write_artifact_index "$branch" "$artifact_tmp_dir" "$effective_build_command" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }

    rm -rf -- "$artifact_dir" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    mv "$artifact_tmp_dir" "$artifact_dir" || {
        rm -rf -- "$artifact_tmp_dir"
        return 1
    }
    echo "persisted build artifacts:"
    echo "file://$artifact_dir"
    if [ -n "$artifact_http_server_url" ]; then
        echo "${artifact_http_server_url}branches/$artifact_key/index.html"
    fi
}

prune_build_artifacts() {
    local results_file
    local artifacts_root
    local branches_dir
    local keep_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key
    local artifact_dir

    artifacts_root=$(build_artifacts_root)
    branches_dir="$artifacts_root/branches"
    if [ ! -d "$branches_dir" ]; then
        return 0
    fi

    results_file=$(build_results_file)
    keep_file=$(mktemp "$artifacts_root/keep.XXXXXX")
    if [ -f "$results_file" ]; then
        while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
            normalize_build_result_fields
            echo "$artifact_key" >>"$keep_file"
        done <"$results_file"
    fi

    for artifact_dir in "$branches_dir"/*; do
        if [ ! -d "$artifact_dir" ]; then
            continue
        fi
        artifact_key=$(basename "$artifact_dir")
        if ! grep -Fxq -- "$artifact_key" "$keep_file"; then
            rm -rf -- "$artifact_dir"
        fi
    done

    rm -f "$keep_file"
    write_artifacts_root_index
}

record_build_result() {
    local branch="$1"
    local status="$2"
    local duration="${3:-}"
    local timestamp="${4:-}"
    local artifact_key="${5:-}"
    local results_file
    local results_dir
    local tmp_file
    local commit

    results_file=$(build_results_file)
    results_dir=$(dirname "$results_file")
    mkdir -p "$results_dir"

    tmp_file=$(mktemp "$results_dir/build-results.XXXXXX")
    commit=$(git rev-parse HEAD)
    if [ -z "$timestamp" ]; then
        timestamp=$(date -Iseconds)
    fi
    if [ -z "$artifact_key" ]; then
        artifact_key=$(build_artifact_branch_key "$branch")
    fi

    if [ -f "$results_file" ]; then
        awk -F '\t' \
            -v artifact_key="$artifact_key" \
            -v branch="$branch" \
            '($6 == "" && artifact_key == $1) || $6 == artifact_key || ($1 == branch && ($3 == "pending" || $3 == "running")) { next } { print }' \
            "$results_file" >"$tmp_file"
    fi

    printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$branch" "$commit" "$status" "$timestamp" "$duration" "$artifact_key" >>"$tmp_file"
    mv "$tmp_file" "$results_file"
    publish_gitea_build_status "$commit" "$status" "$branch" "$artifact_key" || true
    prune_build_results
}

prune_build_results() {
    local results_file
    local results_dir
    local tmp_file
    local filtered_file
    local sorted_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key
    local previous_branch=
    local cutoff_epoch
    local timestamp_epoch

    results_file=$(build_results_file)
    if [ ! -f "$results_file" ]; then
        prune_build_artifacts
        write_artifacts_root_index
        return 0
    fi

    results_dir=$(dirname "$results_file")
    tmp_file=$(mktemp "$results_dir/build-results.XXXXXX")
    filtered_file=$(mktemp "$results_dir/build-results.XXXXXX")
    sorted_file=$(mktemp "$results_dir/build-results.XXXXXX")

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        if git show-ref --quiet --verify "refs/remotes/origin/$branch"; then
            normalize_build_result_fields
            printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$branch" "$commit" "$status" "$timestamp" "$duration" "$artifact_key" >>"$filtered_file"
        fi
    done <"$results_file"

    sort -t $'\t' -k1,1 -k4,4r "$filtered_file" >"$sorted_file"

    if artifact_build_retention_is_count; then
        awk -F '\t' -v limit="$artifact_build_retention_per_branch" '++seen[$1] <= limit' "$sorted_file" >"$tmp_file"
    else
        cutoff_epoch=$(artifact_build_retention_cutoff_epoch)
        while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
            normalize_build_result_fields
            if [ "$branch" != "$previous_branch" ]; then
                printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$branch" "$commit" "$status" "$timestamp" "$duration" "$artifact_key" >>"$tmp_file"
                previous_branch="$branch"
                continue
            fi
            timestamp_epoch=$(date -d "$timestamp" +%s 2>/dev/null || echo 0)
            if [ "$timestamp_epoch" -ge "$cutoff_epoch" ]; then
                printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$branch" "$commit" "$status" "$timestamp" "$duration" "$artifact_key" >>"$tmp_file"
            fi
        done <"$sorted_file"
    fi

    mv "$tmp_file" "$results_file"
    rm -f "$filtered_file" "$sorted_file"
    prune_build_artifacts
    write_artifacts_root_index
}

mark_running_builds_interrupted() {
    local results_file
    local results_dir
    local tmp_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key
    local interrupted_count=0
    local superseded_pending_count=0
    local -A latest_timestamp_by_branch=()

    results_file=$(build_results_file)
    [ -f "$results_file" ] || return 0

    results_dir=$(dirname "$results_file")
    tmp_file=$(mktemp "$results_dir/build-results.XXXXXX") || return 1

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ -z "${latest_timestamp_by_branch[$branch]:-}" ] ||
            [[ "$timestamp" > "${latest_timestamp_by_branch[$branch]}" ]]; then
            latest_timestamp_by_branch[$branch]="$timestamp"
        fi
    done <"$results_file"

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ "$status" = running ]; then
            status=interrupted
            interrupted_count=$((interrupted_count + 1))
            publish_gitea_build_status "$commit" "$status" "$branch" "$artifact_key" || true
        elif [ "$status" = pending ] && [[ "$timestamp" < "${latest_timestamp_by_branch[$branch]}" ]]; then
            status=interrupted
            superseded_pending_count=$((superseded_pending_count + 1))
            publish_gitea_build_status "$commit" "$status" "$branch" "$artifact_key" || true
        fi
        printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$branch" "$commit" "$status" "$timestamp" "$duration" "$artifact_key" >>"$tmp_file"
    done <"$results_file"

    if [ "$interrupted_count" -eq 0 ] && [ "$superseded_pending_count" -eq 0 ]; then
        rm -f "$tmp_file"
        return 0
    fi

    mv "$tmp_file" "$results_file"
    if [ "$interrupted_count" -gt 0 ]; then
        echo "Marked $interrupted_count stale running build(s) as interrupted."
    fi
    if [ "$superseded_pending_count" -gt 0 ]; then
        echo "Marked $superseded_pending_count superseded pending build(s) as interrupted."
    fi
    write_current_build_page
    write_artifacts_root_index
}

is_restartable_build_status() {
    case "$1" in
        running|interrupted|pending)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

latest_build_status_for_branch() {
    local wanted_branch="$1"
    local results_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key

    results_file=$(build_results_file)
    [ -f "$results_file" ] || return 1

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ "$branch" = "$wanted_branch" ]; then
            effective_build_status "$commit" "$status"
            return 0
        fi
    done < <(sort -t $'\t' -k4,4r "$results_file")

    return 1
}

branch_has_restartable_build() {
    local branch="$1"
    local status

    status=$(latest_build_status_for_branch "$branch") || return 1
    is_restartable_build_status "$status"
}

branch_has_failed_build() {
    local branch="$1"
    local status

    status=$(latest_build_status_for_branch "$branch") || return 1
    [ "$status" = failed ]
}

restartable_build_branches() {
    local results_file
    local branch
    local commit
    local status
    local local_status
    local effective_status
    local timestamp
    local duration
    local artifact_key
    local -A seen=()

    results_file=$(build_results_file)
    if [ ! -f "$results_file" ]; then
        echo "No build results file found; no restartable builds to scan." >&2
        return 0
    fi

    echo "Scanning latest build results for restartable builds ..." >&2

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ -n "${seen[$branch]:-}" ]; then
            continue
        fi
        seen[$branch]=true
        local_status="$status"
        effective_status=$(effective_build_status "$commit" "$status")
        if ! branch_exists_on_origin "$branch"; then
            if is_restartable_build_status "$local_status" || is_restartable_build_status "$effective_status"; then
                echo "Skipping restartable build for $branch: branch no longer exists on origin." >&2
            fi
            continue
        fi
        if is_restartable_build_status "$effective_status"; then
            echo "Found restartable build: $branch ($effective_status)." >&2
            echo "$branch"
        elif is_restartable_build_status "$local_status"; then
            echo "Skipping locally $local_status build for $branch: effective status is $effective_status." >&2
        fi
    done < <(sort -t $'\t' -k4,4r "$results_file")
}

failed_build_branches() {
    local results_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key
    local -A seen=()

    results_file=$(build_results_file)
    [ -f "$results_file" ] || return 0

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ -n "${seen[$branch]:-}" ]; then
            continue
        fi
        seen[$branch]=true
        status=$(effective_build_status "$commit" "$status")
        if [ "$status" = failed ] && branch_exists_on_origin "$branch"; then
            echo "$branch"
        fi
    done < <(sort -t $'\t' -k4,4r "$results_file")
}

next_pending_build_branch() {
    local results_file
    local branch
    local commit
    local status
    local timestamp
    local duration
    local artifact_key
    local -A seen=()

    results_file=$(build_results_file)
    [ -f "$results_file" ] || return 1

    while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
        normalize_build_result_fields
        if [ -n "${seen[$branch]:-}" ]; then
            continue
        fi
        seen[$branch]=true
        status=$(effective_build_status "$commit" "$status")
        if [ "$status" = pending ] && branch_exists_on_origin "$branch"; then
            if ! branch_matches_current_worktree_branch "$branch"; then
                continue
            fi
            echo "Picking up pending build: $branch" >&2
            echo "$branch"
            return 0
        fi
    done < <(sort -t $'\t' -k4,4r "$results_file")

    return 1
}

fetch_origin() {
    git_with_gitea_token fetch --prune origin >/dev/null || return 1
    prune_build_results
}

retry_fetch_origin() {
    until fetch_origin; do
        echo "checking origin failed; retrying in 10s ..." >&2
        sleep 10
    done
}

print_build_results() {
    local results_file
    local branch
    local commit
    local status
    local display_status
    local timestamp
    local duration
    local artifact_key
    local has_results=false
    local green=""
    local yellow=""
    local red=""
    local blue=""
    local reset=""

    prune_build_results

    if [ -t 1 ] && command -v tput >/dev/null 2>&1; then
        green=$(tput setaf 2)
        yellow=$(tput setaf 3)
        red=$(tput setaf 1)
        blue=$(tput setaf 4)
        reset=$(tput sgr0)
    fi

    results_file=$(build_results_file)
    echo
    print_build_banner "latest build results:"
    if [ -f "$results_file" ]; then
        while IFS=$'\t' read -r branch commit status timestamp duration artifact_key; do
            normalize_build_result_fields
            display_status=$(effective_build_status "$commit" "$status")
            case "$display_status" in
                success|passed)
                    echo "${green}success: $branch${reset}"
                    has_results=true
                    ;;
                failed)
                    echo "${red}failed: $branch${reset}"
                    has_results=true
                    ;;
                interrupted)
                    echo "${yellow}interrupted: $branch${reset}"
                    has_results=true
                    ;;
                cancelled)
                    echo "${yellow}cancelled: $branch${reset}"
                    has_results=true
                    ;;
                running)
                    echo "${blue}running: $branch${reset}"
                    has_results=true
                    ;;
                pending)
                    echo "${yellow}pending: $branch${reset}"
                    has_results=true
                    ;;
            esac
        done <"$results_file"
    fi

    if [ "$has_results" = false ]; then
        echo "(none)"
    fi
    printf '%*s\n' 80 '' | tr ' ' '-'
    echo
}

print_build_summary() {
    local branch="$1"
    local status="$2"

    echo "BUILD $status: $branch"
}

branch_config_value_or_bootstrap() {
    local primary_name="$1"
    local fallback_name="$2"
    local bootstrap_value="$3"
    local branch_value

    if branch_value=$(branch_config_value "$primary_name" "$fallback_name"); then
        printf '%s' "$branch_value"
    else
        printf '%s' "$bootstrap_value"
    fi
}

resolve_branch_docker_config() {
    docker_build_image=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_IMAGE HSADMIN_NG_BUILD_IMAGE "$bootstrap_docker_build_image")
    docker_build_dockerfile=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKERFILE "" "$bootstrap_docker_build_dockerfile")
    docker_build_context=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_CONTEXT "" "$bootstrap_docker_build_context")
    docker_build_network=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_NETWORK HSADMIN_NG_BUILD_NETWORK "$bootstrap_docker_build_network")
    docker_build_preflight_command=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND "" "$bootstrap_docker_build_preflight_command")
    docker_build_env=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_ENV "" "$bootstrap_docker_build_env")
    docker_build_java_tool_options=$(branch_config_value_or_bootstrap \
        GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS "" "$bootstrap_docker_build_java_tool_options")
}

file_sha256() {
    local file="$1"

    if [ ! -f "$file" ]; then
        echo "ERROR: Dockerfile not found: $file" >&2
        return 1
    fi

    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$file" | awk '{ print $1 }'
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256 "$file" | awk '{ print $1 }'
    else
        echo "ERROR: cannot calculate Dockerfile checksum; sha256sum or shasum is required." >&2
        return 1
    fi
}

text_sha256() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum | awk '{ print $1 }'
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256 | awk '{ print $1 }'
    else
        echo "ERROR: cannot calculate checksum; sha256sum or shasum is required." >&2
        return 1
    fi
}

docker_build_inputs_sha256() {
    local dockerfile_hash

    dockerfile_hash=$(file_sha256 "$docker_build_dockerfile") || return 1
    printf '%s\n%s\n%s\n' "$dockerfile_hash" "$docker_build_dockerfile" "$docker_build_context" | text_sha256
}

ensure_docker_build_image() {
    local dockerfile_hash
    local build_inputs_hash
    local image_build_inputs_hash

    if ! command -v docker >/dev/null 2>&1; then
        echo "ERROR: --docker requires docker in PATH." >&2
        return 1
    fi

    dockerfile_hash=$(file_sha256 "$docker_build_dockerfile") || return 1
    build_inputs_hash=$(docker_build_inputs_sha256) || return 1

    if docker image inspect "$docker_build_image" >/dev/null 2>&1; then
        image_build_inputs_hash=$(docker image inspect "$docker_build_image" \
            --format '{{ index .Config.Labels "org.gittally.build-inputs-sha256" }}' 2>/dev/null || true)
        if [ "$image_build_inputs_hash" = "$build_inputs_hash" ]; then
            return 0
        fi
        echo "Docker build image is stale: $docker_build_image"
        echo "Docker build input checksum changed or is missing on the image label."
    else
        echo "Docker build image not found: $docker_build_image"
    fi

    echo "Building Docker image from $docker_build_dockerfile ..."
    docker build \
        --label "org.gittally.dockerfile=$docker_build_dockerfile" \
        --label "org.gittally.dockerfile-sha256=$dockerfile_hash" \
        --label "org.gittally.build-context=$docker_build_context" \
        --label "org.gittally.build-inputs-sha256=$build_inputs_hash" \
        -t "$docker_build_image" \
        -f "$docker_build_dockerfile" \
        "$docker_build_context" || return 1
}

run_build_command() {
    local branch="$1"
    local effective_build_command="$2"

    if [ "$use_docker_build" = true ]; then
        run_build_command_in_docker "$branch" "$effective_build_command"
    else
        if [ -n "$build_clean_command" ]; then
            branch="$branch" bash -c "$build_clean_command" || return 1
        fi
        branch="$branch" bash -c "$effective_build_command"
    fi
}

docker_gradle_user_home_volume_name() {
    echo "gittally-gradle-$(safe_container_name_part "$(repository_key)")"
}

docker_build_container_name() {
    echo "gittally-build-$(safe_container_name_part "$(repository_key)")"
}

prepare_docker_gradle_volume() {
    local gradle_user_home_volume="$1"
    local uid
    local gid

    uid=$(id -u)
    gid=$(id -g)
    echo "Preparing Docker Gradle cache volume: $gradle_user_home_volume ..."
    docker volume create "$gradle_user_home_volume" >/dev/null || {
        echo "ERROR: cannot create Docker Gradle cache volume: $gradle_user_home_volume" >&2
        return 1
    }
    docker run --rm --user 0 \
        --volume "$gradle_user_home_volume:/gradle-user-home" \
        "$docker_build_image" \
        sh -c 'mkdir -p /gradle-user-home/wrapper/dists && chown -R "$1:$2" /gradle-user-home && chmod -R u+rwX /gradle-user-home' \
        sh "$uid" "$gid" || {
            echo "ERROR: cannot prepare Docker Gradle cache volume: $gradle_user_home_volume" >&2
            return 1
        }
}

prepare_docker_workspace_build_dir() {
    local uid
    local gid

    uid=$(id -u)
    gid=$(id -g)
    echo "Preparing Docker workspace build directory ..."
    docker run --rm --user 0 \
        --workdir "$PWD" \
        --volume "$PWD:$PWD" \
        "$docker_build_image" \
        sh -c '
            bash -c "$3" &&
            mkdir -p build &&
            chown -R "$1:$2" build &&
            chmod -R u+rwX build
        ' \
        sh "$uid" "$gid" "$build_clean_command" || {
            echo "ERROR: cannot prepare Docker workspace build directory: $PWD/build" >&2
            return 1
        }
}

repair_docker_workspace_ownership() {
    local uid
    local gid

    uid=$(id -u)
    gid=$(id -g)
    docker run --rm --user 0 \
        --workdir "$PWD" \
        --volume "$PWD:$PWD" \
        "$docker_build_image" \
        sh -c '
            for path in build .gradle; do
                if [ -e "$path" ]; then
                    chown -R "$1:$2" "$path" &&
                    chmod -R u+rwX "$path"
                fi
            done
        ' \
        sh "$uid" "$gid" || {
            echo "WARNING: could not repair Docker workspace ownership." >&2
            return 1
        }
}

run_build_command_in_docker() {
    local branch="$1"
    local effective_build_command="$2"
    local docker_host="${DOCKER_HOST:-}"
    local container_docker_host="$docker_host"
    local docker_container_user
    local docker_socket_path=
    local docker_socket_group=
    local build_exit_code
    local gradle_user_home_volume
    local build_container_name
    local env_name
    local build_env_assignment
    local testcontainers_host_override="${TESTCONTAINERS_HOST_OVERRIDE:-}"
    local java_tool_options="${JAVA_TOOL_OPTIONS:-}"
    local -a docker_args
    local -a docker_label_args
    local -a build_env_assignments=()

    if [ -z "$docker_host" ]; then
        if [ -S "/run/user/$(id -u)/docker.sock" ]; then
            docker_host="unix:///run/user/$(id -u)/docker.sock"
        else
            docker_host="unix:///var/run/docker.sock"
        fi
        container_docker_host="$docker_host"
    fi

    ensure_docker_build_image || return 1

    gradle_user_home_volume=$(docker_gradle_user_home_volume_name)
    prepare_docker_gradle_volume "$gradle_user_home_volume" || return 1
    prepare_docker_workspace_build_dir || return 1

    build_container_name=$(docker_build_container_name)
    remove_docker_container_by_name "$build_container_name"
    docker_args=(run --rm --name "$build_container_name")
    mapfile -t docker_label_args < <(gittally_docker_label_args build)
    docker_args+=("${docker_label_args[@]}")
    if [ -t 0 ]; then
        docker_args+=(--interactive)
    fi
    if [ -t 1 ]; then
        docker_args+=(--tty)
    fi
    docker_container_user=0

    docker_args+=(
        --workdir "$PWD"
        --volume "$PWD:$PWD"
        --volume "$gradle_user_home_volume:/gradle-user-home"
        --env "HOME=/tmp/docker-home"
        --env "GRADLE_USER_HOME=/gradle-user-home"
        --env "branch=$branch"
    )

    if [ -n "$docker_build_env" ]; then
        read -r -a build_env_assignments <<<"$docker_build_env"
        for build_env_assignment in "${build_env_assignments[@]}"; do
            docker_args+=(--env "$build_env_assignment")
        done
    fi

    if [ -n "$docker_build_network" ]; then
        docker_args+=(--network "$docker_build_network")
    fi

    if [[ "$docker_host" == unix://* ]]; then
        docker_socket_path="${docker_host#unix://}"
        if [ ! -S "$docker_socket_path" ]; then
            echo "ERROR: Docker socket not found: $docker_socket_path" >&2
            return 1
        fi
        if [ "$docker_socket_path" = "/run/user/$(id -u)/docker.sock" ]; then
            docker_container_user="$(id -u)"
        fi
        container_docker_host="unix:///var/run/docker.sock"
        docker_args+=(
            --volume "$docker_socket_path:/var/run/docker.sock"
            --env "TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock"
        )
        docker_socket_group=$(stat -c '%g' "$docker_socket_path" 2>/dev/null || true)
        if [ -n "$docker_socket_group" ] && [ "$docker_socket_path" != "/run/user/$(id -u)/docker.sock" ]; then
            docker_args+=(--group-add "$docker_socket_group")
        fi
    fi

    docker_args+=(--user "$docker_container_user")

    docker_args+=(--env "DOCKER_HOST=$container_docker_host")
    if [ -z "$testcontainers_host_override" ]; then
        if [ "$docker_build_network" = host ]; then
            testcontainers_host_override=localhost
        else
            testcontainers_host_override=host.docker.internal
            docker_args+=(--add-host "host.docker.internal:host-gateway")
        fi
    fi
    docker_args+=(--env "TESTCONTAINERS_HOST_OVERRIDE=$testcontainers_host_override")
    java_tool_options="$java_tool_options $docker_build_java_tool_options"
    java_tool_options="$java_tool_options -Dtestcontainers.host.override=$testcontainers_host_override"
    docker_args+=(--env "JAVA_TOOL_OPTIONS=$java_tool_options")

    for env_name in \
        HSADMINNG_POSTGRES_ADMIN_USERNAME \
        HSADMINNG_POSTGRES_RESTRICTED_USERNAME \
        HSADMINNG_MIGRATION_DATA_PATH \
        TESTCONTAINERS_LOG_LEVEL; do
        if [ -n "${!env_name+x}" ]; then
            docker_args+=(--env "$env_name")
        fi
    done

    echo "Checking Docker access inside build container ..."
    if [ -n "$docker_build_preflight_command" ] &&
        ! docker "${docker_args[@]}" "$docker_build_image" bash -c "$docker_build_preflight_command" >/dev/null; then
        echo "ERROR: Docker is not reachable from inside the build container." >&2
        echo "       DOCKER_HOST inside container: $container_docker_host" >&2
        echo "       TESTCONTAINERS_HOST_OVERRIDE inside container: $testcontainers_host_override" >&2
        return 1
    fi

    echo "Docker image: $docker_build_image"
    if [[ "$container_docker_host" == unix://* ]]; then
        docker "${docker_args[@]}" "$docker_build_image" \
            sh -c 'mkdir -p "$HOME" && {
                printf "%s\n" "docker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy"
                printf "%s\n" "docker.host=unix:///var/run/docker.sock"
                printf "%s\n" "testcontainers.docker.socket.override=/var/run/docker.sock"
                printf "%s\n" "testcontainers.host.override=$TESTCONTAINERS_HOST_OVERRIDE"
            } >"$HOME/.testcontainers.properties" && exec bash -c "$1"' \
            sh "$effective_build_command"
    else
        docker "${docker_args[@]}" "$docker_build_image" bash -c "$effective_build_command"
    fi
    build_exit_code=$?
    repair_docker_workspace_ownership || true
    return "$build_exit_code"
}

branch_config_build_command() {
    local checkout_repo_root
    local config_file
    local value_file
    local status

    checkout_repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
    config_file="$checkout_repo_root/.gitTally"
    if [ ! -f "$config_file" ]; then
        return 1
    fi

    value_file=$(mktemp "${TMPDIR:-/tmp}/gittally-build-command.XXXXXX") || return 1
    (
        unset GITTALLY_BUILD_COMMAND
        set -a
        # shellcheck source=/dev/null
        . "$config_file" >/dev/null
        set +a
        if [ -n "${GITTALLY_BUILD_COMMAND+x}" ]; then
            printf '%s' "$GITTALLY_BUILD_COMMAND" >"$value_file"
        else
            exit 1
        fi
    )
    status=$?

    if [ "$status" -eq 0 ]; then
        cat "$value_file"
    fi
    rm -f "$value_file"
    return "$status"
}

build_command_for_current_checkout() {
    local branch_build_command

    if branch_build_command=$(branch_config_build_command); then
        printf '%s' "$branch_build_command"
    else
        printf '%s' "$environment_build_command"
    fi
}

build_current_checkout() {
    local branch
    local build_exit_code
    local effective_build_command
    local started_at
    local ended_at
    local started_timestamp
    local ended_timestamp
    local build_duration
    local artifact_key
    local build_lock_fd=
    local build_lock_path
    local build_lock_dir
    local artifacts_root
    local current_log_file
    local build_stdout_file
    local build_stderr_file
    local build_stdout_pipe
    local build_stderr_pipe
    local tee_stdout_pid
    local tee_stderr_pid

    branch=$(git branch --show-current)
    if [ -z "$branch" ]; then
        branch="detached HEAD"
    fi

    effective_build_command=$(build_command_for_current_checkout) || return 1
    if [ "$use_docker_build" = true ]; then
        resolve_branch_docker_config
    fi

    print_build_banner "building branch: $branch"
    echo "working directory: $PWD"
    if [[ "$effective_build_command" == *"./gradlew"* ]] && [ ! -x ./gradlew ]; then
        echo "ERROR: ./gradlew not found or not executable in $PWD" >&2
        return 1
    fi
    if [[ "$effective_build_command" == *"./gradlew"* ]]; then
        echo "gradle wrapper: $(realpath ./gradlew)"
    fi
    if [ "$use_docker_build" = true ]; then
        echo "build runtime: Docker image $docker_build_image"
    fi
    echo "build command: $effective_build_command"
    if [ "$use_docker_build" != true ] && [ -n "$build_clean_command" ]; then
        echo "clean command: $build_clean_command"
    fi
    started_at=$(date +%s)
    started_timestamp=$(date -Iseconds)
    artifact_key=$(build_artifact_key "$branch" "$started_timestamp")
    active_build_branch="$branch"
    active_build_artifact_key="$artifact_key"
    active_build_started_at="$started_at"
    record_build_result "$branch" pending "" "$started_timestamp" "$artifact_key"
    clear_build_cancel_request
    if ! write_build_cancel_token; then
        echo "WARNING: could not prepare build cancellation token." >&2
    fi
    artifacts_root=$(build_artifacts_root)
    mkdir -p "$artifacts_root" || return 1
    current_log_file=$(current_build_log_file)
    {
        printf 'building branch: %s\n' "$branch"
        printf 'started: %s\n' "$(display_build_timestamp "$started_timestamp")"
        printf 'working directory: %s\n' "$PWD"
        printf 'build command: %s\n' "$effective_build_command"
        if [ "$use_docker_build" != true ] && [ -n "$build_clean_command" ]; then
            printf 'clean command: %s\n' "$build_clean_command"
        fi
        printf '\n\n'
    } >"$current_log_file"
    write_current_build_page "$branch" running "$started_timestamp"
    build_stdout_file=$(mktemp "$artifacts_root/build-stdout.XXXXXX") || return 1
    build_stderr_file=$(mktemp "$artifacts_root/build-stderr.XXXXXX") || {
        rm -f "$build_stdout_file"
        return 1
    }
    build_stdout_pipe=$(mktemp "$artifacts_root/build-stdout-pipe.XXXXXX") || {
        rm -f "$build_stdout_file" "$build_stderr_file"
        return 1
    }
    build_stderr_pipe=$(mktemp "$artifacts_root/build-stderr-pipe.XXXXXX") || {
        rm -f "$build_stdout_file" "$build_stderr_file" "$build_stdout_pipe"
        return 1
    }
    rm -f "$build_stdout_pipe" "$build_stderr_pipe"
    if ! mkfifo "$build_stdout_pipe" "$build_stderr_pipe"; then
        rm -f "$build_stdout_file" "$build_stderr_file" "$build_stdout_pipe" "$build_stderr_pipe"
        return 1
    fi
    build_lock_path=$(build_lock_file)
    build_lock_dir=$(dirname "$build_lock_path")
    mkdir -p "$build_lock_dir"
    if command -v flock >/dev/null 2>&1; then
        exec {build_lock_fd}>"$build_lock_path" || {
            rm -f "$build_stdout_file" "$build_stderr_file" "$build_stdout_pipe" "$build_stderr_pipe"
            return 1
        }
        echo "waiting for build lock: $build_lock_path"
        if ! flock -n "$build_lock_fd"; then
            echo "build lock is already held: $build_lock_path"
            cleanup_stale_build_runtime || true
            terminate_stale_build_lock_holders "$build_lock_path" || true
            echo "waiting up to 30s for build lock: $build_lock_path"
        fi
        if ! flock -w 30 "$build_lock_fd"; then
            echo "ERROR: could not acquire build lock: $build_lock_path" >&2
            exec {build_lock_fd}>&-
            rm -f "$build_stdout_file" "$build_stderr_file" "$build_stdout_pipe" "$build_stderr_pipe"
            return 1
        fi
        echo "acquired build lock: $build_lock_path"
    fi
    tee "$build_stdout_file" <"$build_stdout_pipe" | tee -a "$current_log_file" &
    tee_stdout_pid=$!
    tee "$build_stderr_file" <"$build_stderr_pipe" | tee -a "$current_log_file" >&2 &
    tee_stderr_pid=$!
    record_build_result "$branch" running "" "$started_timestamp" "$artifact_key"
    run_build_command "$branch" "$effective_build_command" >"$build_stdout_pipe" 2>"$build_stderr_pipe" &
    active_build_pid=$!
    wait_for_active_build "$active_build_pid"
    build_exit_code=$?
    active_build_pid=
    clear_build_cancel_request
    wait "$tee_stdout_pid" || true
    wait "$tee_stderr_pid" || true
    rm -f "$build_stdout_pipe" "$build_stderr_pipe"
    ended_at=$(date +%s)
    ended_timestamp=$(date -Iseconds)
    build_duration=$(format_build_duration "$((ended_at - started_at))")
    if [ "$active_build_cancelled" = true ]; then
        echo "build cancelled after $build_duration"
        printf '\nbuild cancelled after %s\n' "$build_duration" >>"$current_log_file"
    else
        echo "build finished with exit code $build_exit_code after $build_duration"
        printf '\nbuild finished with exit code %s after %s\n' "$build_exit_code" "$build_duration" >>"$current_log_file"
    fi
    persist_build_artifacts "$branch" "$build_stdout_file" "$build_stderr_file" "$artifact_key" "$effective_build_command" || \
        echo "WARNING: could not persist build artifacts for branch: $branch" >&2
    rm -f "$build_stdout_file" "$build_stderr_file"
    if [ -n "$build_lock_fd" ]; then
        flock -u "$build_lock_fd" || true
        exec {build_lock_fd}>&-
    fi
    if [ "$active_build_cancelled" = true ]; then
        active_build_branch=
        active_build_artifact_key=
        active_build_started_at=
        active_build_cancelled=false
        record_build_result "$branch" cancelled "$build_duration" "$ended_timestamp" "$artifact_key"
        write_current_build_page "$branch" cancelled "$started_timestamp"
        print_build_summary "$branch" CANCELLED
        print_build_results
    elif [ "$build_exit_code" -eq 0 ]; then
        active_build_branch=
        active_build_artifact_key=
        active_build_started_at=
        record_build_result "$branch" success "$build_duration" "$ended_timestamp" "$artifact_key"
        write_current_build_page "$branch" success "$started_timestamp"
        print_build_summary "$branch" SUCCESS
        print_build_results
    else
        active_build_branch=
        active_build_artifact_key=
        active_build_started_at=
        record_build_result "$branch" failed "$build_duration" "$ended_timestamp" "$artifact_key"
        write_current_build_page "$branch" failed "$started_timestamp"
        print_build_summary "$branch" FAILED
        handle_build_failure_prompt "$branch" "$artifact_key"
        print_build_results
        return 0
    fi
}

has_new_commits() {
    local local_ref="$1"
    local upstream_ref="$2"
    local count

    if ! git show-ref --quiet --verify "$local_ref" || ! git show-ref --quiet --verify "$upstream_ref"; then
        return 1
    fi

    count=$(git rev-list --count "$local_ref..$upstream_ref") || return 2
    [ "${count:-0}" -gt 0 ]
}

changed_local_branches() {
    local branches
    local branch
    local upstream
    local has_new_commits_status

    branches=$(git for-each-ref --format='%(refname:strip=2)' refs/heads) || return 1
    while read -r branch; do
        if [ -z "$branch" ]; then
            continue
        fi

        if ! branch_exists_on_origin "$branch"; then
            continue
        fi

        if ! branch_matches_current_worktree_branch "$branch"; then
            continue
        fi

        upstream=$(git for-each-ref --format='%(upstream)' "refs/heads/$branch") || return 1
        if [ -n "$upstream" ]; then
            if has_new_commits "refs/heads/$branch" "$upstream"; then
                echo "$branch"
            else
                has_new_commits_status=$?
                if [ "$has_new_commits_status" -ne 1 ]; then
                    return "$has_new_commits_status"
                fi
            fi
        elif git show-ref --quiet --verify "refs/remotes/origin/$branch"; then
            if has_new_commits "refs/heads/$branch" "refs/remotes/origin/$branch"; then
                echo "$branch"
            else
                has_new_commits_status=$?
                if [ "$has_new_commits_status" -ne 1 ]; then
                    return "$has_new_commits_status"
                fi
            fi
        fi
    done <<<"$branches"
}

recent_new_origin_branches() {
    local cutoff
    local branches
    local branch
    local commit_date

    if [ "$stay_on_current_branch" = true ]; then
        return 0
    fi

    cutoff=$(new_branch_commit_max_age_cutoff_epoch)
    branches=$(git for-each-ref --sort=-committerdate --format='%(refname:strip=3) %(committerdate:unix)' refs/remotes/origin) || return 1
    while read -r branch commit_date; do
        if [ "$branch" = "HEAD" ]; then
            continue
        fi
        if [ -z "$branch" ]; then
            continue
        fi

        if git show-ref --quiet --verify "refs/heads/$branch"; then
            continue
        fi

        if [ "$commit_date" -lt "$cutoff" ]; then
            if ! grep -Fxq -- "$branch" "$reported_skipped_new_branches"; then
                echo "$branch" >>"$reported_skipped_new_branches"
                echo "skipping new origin branch $branch: latest commit is older than $new_branch_commit_max_age" >&2
            fi
            continue
        fi

        echo "$branch"
    done <<<"$branches"
}

next_branch_to_build() {
    local branches

    branches=$(
        changed_local_branches || exit 1
        recent_new_origin_branches || exit 1
    ) || return 1

    if [ -n "$branches" ]; then
        printf '%s\n' "$branches" | awk '!seen[$0]++' | head -n1
    fi
}

auto_build_check() {
    [ -n "$auto_build_branches" ] || return 0

    local today now_hhmm matched_slot state_file state_dir
    today=$(date -u +%Y-%m-%d)
    now_hhmm=$(date -u +%H:%M)

    # Find the latest configured slot at or before current UTC time.
    matched_slot=""
    local IFS=';'
    local slot
    for slot in $auto_build_times; do
        if [[ ! "$slot" =~ ^[0-2][0-9]:[0-5][0-9]$ ]]; then
            echo "WARNING: skipping invalid auto-build time slot: '$slot'; expected HH:MM." >&2
            continue
        fi
        [[ ! "$slot" > "$now_hhmm" ]] && matched_slot="$slot"
    done
    [ -n "$matched_slot" ] || return 0

    state_file=$(auto_builds_state_file)
    state_dir=$(dirname "$state_file")
    mkdir -p "$state_dir"

    local branch
    for branch in $auto_build_branches; do
        if grep -qF "${branch}"$'\t'"${today}"$'\t'"${matched_slot}" "$state_file" 2>/dev/null; then
            continue
        fi
        printf '%s\t%s\t%s\n' "$branch" "$today" "$matched_slot" >> "$state_file"
        echo "auto build scheduled: $branch (slot $matched_slot)" >&2
        echo "$branch"
        return 0
    done
}

retry_origin_change_check() {
    local branch

    while true; do
        if branch=$(next_pending_build_branch); then
            echo "$branch"
            return 0
        fi
        retry_fetch_origin
        if branch=$(next_branch_to_build); then
            echo "$branch"
            return 0
        fi
        echo "checking for new branches or commits failed; retrying in 10s ..." >&2
        sleep 10
    done
}

restart_interrupted_or_running_builds() {
    local branch
    local has_restartable_build=false

    while IFS= read -r branch; do
        if ! branch_matches_current_worktree_branch "$branch"; then
            continue
        fi
        has_restartable_build=true
        echo "Restarting pending, interrupted, or stale running build: $branch"
        checkout_and_build "$branch" || return 1
    done < <(restartable_build_branches)

    if [ "$has_restartable_build" = true ]; then
        print_build_results
    else
        echo "No restartable pending, interrupted, or stale running builds found."
    fi
}

retry_failed_builds() {
    local branch
    local has_failed_build=false

    if [ "$retry_failed_builds_requested" != true ]; then
        return 0
    fi

    while IFS= read -r branch; do
        if ! branch_matches_current_worktree_branch "$branch"; then
            continue
        fi
        has_failed_build=true
        echo "Retrying failed build: $branch"
        checkout_and_build "$branch" || return 1
    done < <(failed_build_branches)

    if [ "$has_failed_build" = true ]; then
        print_build_results
    fi
}

for arg in "$@"; do
    case "$arg" in
        --install)
            install_after_pull=true
            ;;
        --systemd)
            install_systemd_after_install=true
            systemd_command_given=true
            ;;
        --systemd:start|--systemd:stop|--systemd:reload|--systemd:status|--systemd:log|--systemd:watch|--systemd:enable|--systemd:disable)
            systemd_action="${arg#--systemd:}"
            systemd_command_given=true
            ;;
        --pull)
            pull_current_branch=true
            ;;
        --docker)
            use_docker_build=true
            ;;
        --http)
            use_artifact_http_server=true
            ;;
        --open)
            open_artifact_frontend=true
            use_artifact_http_server=true
            ;;
        --nginx)
            use_artifact_nginx=true
            use_artifact_http_server=true
            ;;
        --retry)
            retry_failed_builds_requested=true
            ;;
        --stay)
            stay_on_current_branch=true
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -*)
            echo "Unknown option: $arg"
            usage
            exit 1
            ;;
        *)
            branches_to_build+=("$(normalize_branch_name "$arg")")
            ;;
    esac
done

if [ -n "$systemd_action" ]; then
    run_systemd_action "$systemd_action"
    exit $?
fi

if [ "$install_systemd_after_install" = true ] && [ "$install_after_pull" != true ]; then
    echo "ERROR: --systemd requires --install." >&2
    exit 1
fi

validate_stay_on_current_branch || exit 1

echo "$tool_name version $script_version"

detect_gitea_repo
configure_artifact_nginx_defaults
validate_artifact_build_retention_per_branch
validate_new_branch_commit_max_age
validate_auto_build_times
if [ "$pull_current_branch" = true ] || [ "$install_after_pull" != true ] || [ "${#branches_to_build[@]}" -gt 0 ]; then
    validate_gitea_git_credentials || exit 1
fi

if gitea_status_enabled; then
    echo "Gitea build status: ${gitea_base_url%/}/$gitea_owner/$gitea_repo ($gitea_status_context)"
elif [ -n "$gitea_token" ]; then
    echo "WARNING: Gitea build status disabled because base URL, owner, repo, curl, or python3 is missing." >&2
fi

if [ "$pull_current_branch" = true ]; then
    pull_current_branch_from_origin || exit 1
fi

if [ "$install_after_pull" = true ]; then
    forwarded_args=()
    for arg in "$@"; do
        case "$arg" in
            --install|--pull|--systemd)
                ;;
            --systemd:*)
                ;;
            *)
                forwarded_args+=("$arg")
                ;;
        esac
    done

    install_to_bin
    if [ "$install_systemd_after_install" = true ]; then
        install_systemd_service "$(dirname "$installed_script_path")/$systemd_unit_name"
    fi
    if [ "${#forwarded_args[@]}" -gt 0 ]; then
        GITTALLY_BIN_FORWARD=true exec "$installed_script_path" "${forwarded_args[@]}"
    fi
    exit 0
fi

if [ "$pull_current_branch" = true ]; then
    exit 0
fi

if [ "$systemd_command_given" = true ]; then
    exit 0
fi

retry_fetch_origin
mark_running_builds_interrupted || exit 1
start_artifact_http_server
open_artifact_frontend_if_requested
start_artifact_nginx
if [ "${#branches_to_build[@]}" -eq 0 ]; then
    restart_interrupted_or_running_builds || exit 1
    retry_failed_builds || exit 1
fi

for branch in "${branches_to_build[@]}"; do
    checkout_requested_branch "$branch" || exit 1
done

start_resource_monitor
write_system_page || true
write_env_page || true
echo "$tool_name version $script_version: service ready"

while true; do
    branch_to_build=$(auto_build_check)
    if [ -z "$branch_to_build" ]; then
        branch_to_build=$(retry_origin_change_check)
    fi

    if [ -n "$branch_to_build" ]; then
          checkout_and_build "$branch_to_build"
          continue
    fi

    # wait 10s with a little animation
    echo -e -n "\r\033[K waiting for changes (/) ..."
    sleep 2
    echo -e -n "\r\033[K waiting for changes (-) ..."
    sleep 2
    echo -e -n "\r\033[K waiting for changes (\) ..."
    sleep 2
    echo -e -n "\r\033[K waiting for changes (|) ..."
    sleep 2
    echo -e -n "\r\033[K waiting for changes ( ) ... "
    sleep 2
    echo -e -n "\r\033[K checking for changes"
done
