# Results dashboard (§4.5, D7): the YunoHost-CI-like overview at ci.commoninternet.net. Reads the # Drone API (read-only) and renders latest-run-per-recipe + SVG badges. Packaged as a Nix-built OCI # image and run as a swarm service on `proxy`, routed by traefik at Host(ci.commoninternet.net) — the # comment-bridge's Host && PathPrefix(`/hook`) rule is longer, so /hook still wins (priority by rule # length). Deployed by an idempotent-reconcile oneshot (same pattern as bridge/drone). { pkgs, ... }: let dashApp = pkgs.runCommand "cc-ci-dashboard-app" { } '' mkdir -p $out/app cp ${../dashboard/dashboard.py} $out/app/dashboard.py ''; # Content-derived tag: changes whenever dashboard.py changes, so `docker stack deploy` actually # rolls the service to the new image (a fixed `:latest` tag + unchanged stack spec does NOT roll — # swarm sees no change). Reproducible + self-healing. imageTag = builtins.substring 0 12 (builtins.hashString "sha256" (builtins.readFile ../dashboard/dashboard.py)); image = pkgs.dockerTools.buildLayeredImage { name = "cc-ci-dashboard"; tag = imageTag; contents = [ pkgs.python3 pkgs.cacert dashApp ]; config = { Cmd = [ "${pkgs.python3}/bin/python3" "/app/dashboard.py" ]; Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]; ExposedPorts = { "8080/tcp" = { }; }; }; }; stack = pkgs.writeText "cc-ci-dashboard-stack.yml" '' version: "3.8" services: app: image: cc-ci-dashboard:${imageTag} environment: - DRONE_URL=https://drone.ci.commoninternet.net - CI_REPO=recipe-maintainers/cc-ci - DASH_LISTEN=0.0.0.0:8080 - DRONE_TOKEN_FILE=/run/secrets/drone_token secrets: - drone_token networks: - proxy deploy: replicas: 1 restart_policy: condition: any labels: - "traefik.enable=true" - "traefik.http.services.ccci-dashboard.loadbalancer.server.port=8080" - "traefik.http.routers.ccci-dashboard.rule=Host(`ci.commoninternet.net`)" - "traefik.http.routers.ccci-dashboard.entrypoints=web-secure" - "traefik.http.routers.ccci-dashboard.tls=true" networks: proxy: external: true secrets: drone_token: external: true name: cc_ci_dashboard_drone_token_v1 ''; reconcile = pkgs.writeShellApplication { name = "cc-ci-reconcile-dashboard"; runtimeInputs = with pkgs; [ docker coreutils ]; text = '' if [ ! -r /run/secrets/bridge_drone_token ]; then echo "FATAL: /run/secrets/bridge_drone_token missing (rebuild ordering?)" >&2 exit 1 fi docker load -i ${image} # Dashboard reads the Drone API read-only; reuse the same Drone token value as the bridge. docker secret inspect cc_ci_dashboard_drone_token_v1 >/dev/null 2>&1 \ || docker secret create cc_ci_dashboard_drone_token_v1 /run/secrets/bridge_drone_token >/dev/null docker stack deploy --detach=true -c ${stack} ccci-dashboard ''; }; in { systemd.services.deploy-dashboard = { description = "Reconcile the cc-ci results dashboard (overview + badges) swarm service"; # Serialized after deploy-bridge (chain proxy→drone→bridge→dashboard→backupbot) to avoid the # concurrent abra-init race on a fresh host (see bridge.nix). Ordering-only. after = [ "deploy-bridge.service" "deploy-proxy.service" "swarm-init.service" "docker.service" "network-online.target" ]; requires = [ "swarm-init.service" "docker.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${reconcile}/bin/cc-ci-reconcile-dashboard"; }; }; }