Skill: highlander89/billy-mission-control-visual-qa (version 0.1.3)

--- name: mission-control-visual-qa description: Run Mission Control visual QA on SAPCONET over SSH using Puppeteer screenshots and basic DOM checks. --- # mission-control-visual-qa Author: Billy (SAPCONET) ## Purpose Run visual QA (screenshots + basic DOM checks) for Mission Control pages on SAPCONET via SSH (Neill machine `100.110.24.44`). ## What this skill includes - `scripts/mission-control-visual-qa.js`: Puppeteer-based remote runner (intended to run on SAPCONET). - `scripts/run-mission-control-visual-qa.sh`: Local helper that copies and runs the Node script over `scp` + `ssh`. ## Safety rules - Only target Mission Control pages you are authorized to inspect. - Default output path is `~/.openclaw/workspace/output/visual-qa/` on SAPCONET. - No external network activity is performed by scripts other than SSH/SCP to SAPCONET and page loads for supplied URLs. - Script is read-only and does not submit forms or click destructive controls. ## Usage From local machine: ```bash bash scripts/run-mission-control-visual-qa.sh \ "https://mission-control.example.local/dashboard" \ "https://mission-control.example.local/status" ``` Optional env vars: - `SSH_TARGET` (default: `neill@100.110.24.44`) - `REMOTE_RUN_DIR` (default: `~/.openclaw/workspace/mission-control-visual-qa-runner`) - `OUTPUT_DIR` (default: `~/.openclaw/workspace/output/visual-qa/`) ## Expected output On SAPCONET host, each URL produces: - `*.png` screenshot - basic DOM result (`title` + presence of `main`, `h1`, and body text) - final JSON summary printed to stdout

path: scripts/mission-control-visual-qa.js
content: #!/usr/bin/env node
'use strict';

const fs = require('fs');
const os = require('os');
const path = require('path');

function sanitizeName(input) {
  return input.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120);
}

function nowStamp() {
  const d = new Date();
  const pad = (n) => String(n).padStart(2, '0');
  return (
    d.getUTCFullYear() +
    pad(d.getUTCMonth() + 1) +
    pad(d.getUTCDate()) +
    'T' +
    pad(d.getUTCHours()) +
    pad(d.getUTCMinutes()) +
    pad(d.getUTCSeconds()) +
    'Z'
  );
}

async function main() {
  const urls = process.argv.slice(2);
  if (urls.length === 0) {
    console.error('Usage: node mission-control-visual-qa.js <url1> [url2 ...]');
    process.exit(2);
  }

  const outputDir = process.env.OUTPUT_DIR || path.join(os.homedir(), '.openclaw/workspace/output/visual-qa');
  fs.mkdirSync(outputDir, { recursive: true });

  let puppeteer;
  try {
    puppeteer = require('puppeteer');
  } catch (err) {
    console.error('Missing dependency: puppeteer. Install on SAPCONET host.');
    console.error(err.message);
    process.exit(3);
  }

  const browser = await puppeteer.launch({
    headless: true,
    executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  });

  const results = [];

  try {
    for (const url of urls) {
      const page = await browser.newPage();
      await page.setViewport({ width: 1440, height: 900 });

      const item = {
        url,
        ok: false,
        checks: {},
      };

      try {
        await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
        const meta = await page.evaluate(() => {
          const text = document.body ? document.body.innerText || '' : '';
          return {
            title: document.title || '',
            hasMain: Boolean(document.querySelector('main')),
            hasH1: Boolean(document.querySelector('h1')),
            bodyTextLength: text.trim().length,
          };
        });

        item.checks = {
          titleNonEmpty: meta.title.length > 0,
          hasMain: meta.hasMain,
          hasH1: meta.hasH1,
          bodyTextLengthGt100: meta.bodyTextLength > 100,
          title: meta.title,
          bodyTextLength: meta.bodyTextLength,
        };

        item.ok =
          item.checks.titleNonEmpty &&
          item.checks.hasMain &&
          item.checks.hasH1 &&
          item.checks.bodyTextLengthGt100;

        const fileName = `${nowStamp()}_${sanitizeName(new URL(url).hostname + new URL(url).pathname)}.png`;
        const screenshotPath = path.join(outputDir, fileName);
        await page.screenshot({ path: screenshotPath, fullPage: true });
        item.screenshotPath = screenshotPath;
      } catch (err) {
        item.error = err.message;
      } finally {
        await page.close();
      }

      results.push(item);
    }
  } finally {
    await browser.close();
  }

  const summary = {
    ranAt: new Date().toISOString(),
    outputDir,
    total: results.length,
    passed: results.filter((r) => r.ok).length,
    failed: results.filter((r) => !r.ok).length,
    results,
  };

  console.log(JSON.stringify(summary, null, 2));
  process.exit(summary.failed > 0 ? 1 : 0);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});


path: scripts/run-mission-control-visual-qa.sh
content: #!/usr/bin/env bash
set -euo pipefail

if [[ "$#" -lt 1 ]]; then
  echo "Usage: $0 <url1> [url2 ...]"
  exit 2
fi

SSH_TARGET="${SSH_TARGET:-neill@100.110.24.44}"
REMOTE_RUN_DIR="${REMOTE_RUN_DIR:-~/.openclaw/workspace/mission-control-visual-qa-runner}"
OUTPUT_DIR="${OUTPUT_DIR:-~/.openclaw/workspace/output/visual-qa/}"
SCRIPT_NAME="mission-control-visual-qa.js"
LOCAL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

ssh "${SSH_TARGET}" "mkdir -p ${REMOTE_RUN_DIR} ${OUTPUT_DIR}"
scp "${LOCAL_SCRIPT_DIR}/${SCRIPT_NAME}" "${SSH_TARGET}:${REMOTE_RUN_DIR}/${SCRIPT_NAME}"

quoted_args=()
for arg in "$@"; do
  quoted_args+=("$(printf '%q' "$arg")")
done

# shellcheck disable=SC2029
ssh "${SSH_TARGET}" "cd ${REMOTE_RUN_DIR} && OUTPUT_DIR='${OUTPUT_DIR}' node ./${SCRIPT_NAME} ${quoted_args[*]}"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)