This is an automated email from the ASF dual-hosted git repository. martinzink pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
commit fbe5cc5d5c5a9b9da1c3b236a0cb35177b89d203 Author: Gabor Gyimesi <[email protected]> AuthorDate: Thu Apr 9 15:29:12 2026 +0200 MINIFICPP-2737 Add weekly CI workflow runs and status page All Github Actions workflows are run weekly to avoid regression. The workflow run results are visible on the status page hosted on Github Pages. Status page example can be checked at: https://lordgamez.github.io/nifi-minifi-cpp/status Closes #2128 Signed-off-by: Martin Zink <[email protected]> --- .asf.yaml | 3 + .github/workflows/ci.yml | 7 +- .github/workflows/compiler-support.yml | 5 +- .github/workflows/create-release-artifacts.yml | 5 +- .github/workflows/memcheck_ci.yml | 5 +- .github/workflows/verify-package.yml | 28 +- README.md | 3 +- docs/status/index.html | 661 +++++++++++++++++++++++++ 8 files changed, 706 insertions(+), 11 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index 1e0bbef39..ef4c57936 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -33,3 +33,6 @@ notifications: commits: [email protected] issues: [email protected] jira_options: link label worklog +github: + ghp_branch: main + ghp_path: /docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96595f786..251ed4197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,10 @@ name: "MiNiFi-CPP CI" -on: [push, pull_request, workflow_dispatch] +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 1 * * 1' env: DOCKER_CMAKE_FLAGS: -DDOCKER_VERIFY_THREAD=3 -DUSE_SHARED_LIBS= -DSTRICT_GSL_CHECKS=AUDIT -DCI_BUILD=ON -DENABLE_AWS=ON -DENABLE_KAFKA=ON -DENABLE_MQTT=ON -DENABLE_AZURE=ON -DENABLE_SQL=ON \ -DENABLE_SPLUNK=ON -DENABLE_GCP=ON -DENABLE_OPC=ON -DENABLE_PYTHON_SCRIPTING=ON -DENABLE_LUA_SCRIPTING=ON -DENABLE_KUBERNETES=ON -DENABLE_TEST_PROCESSORS=ON -DENABLE_PROMETHEUS=ON \ diff --git a/.github/workflows/compiler-support.yml b/.github/workflows/compiler-support.yml index 8179737df..5cbf79535 100644 --- a/.github/workflows/compiler-support.yml +++ b/.github/workflows/compiler-support.yml @@ -2,7 +2,10 @@ name: 'Check supported Compilers' -on: [workflow_dispatch] +on: + workflow_dispatch: + schedule: + - cron: '0 1 * * 1' jobs: gcc-build: diff --git a/.github/workflows/create-release-artifacts.yml b/.github/workflows/create-release-artifacts.yml index 59118285a..4980bac58 100644 --- a/.github/workflows/create-release-artifacts.yml +++ b/.github/workflows/create-release-artifacts.yml @@ -1,6 +1,9 @@ name: "Create Release Artifacts" -on: [workflow_dispatch] +on: + workflow_dispatch: + schedule: + - cron: '0 1 * * 1' env: CMAKE_FLAGS: -DCMAKE_BUILD_TYPE=Release -DCI_BUILD=OFF -DENABLE_ALL=ON -DMINIFI_FAIL_ON_WARNINGS=OFF -DDOCKER_BUILD_ONLY=ON -DSKIP_TESTS=ON diff --git a/.github/workflows/memcheck_ci.yml b/.github/workflows/memcheck_ci.yml index a31471623..ad728dadc 100644 --- a/.github/workflows/memcheck_ci.yml +++ b/.github/workflows/memcheck_ci.yml @@ -1,5 +1,8 @@ name: "MiNiFi-CPP memcheck" -on: [workflow_dispatch] +on: + workflow_dispatch: + schedule: + - cron: '0 1 * * 1' env: CMAKE_FLAGS: >- -DCMAKE_BUILD_TYPE=Debug diff --git a/.github/workflows/verify-package.yml b/.github/workflows/verify-package.yml index 41da7fa00..e3b8f8bd1 100644 --- a/.github/workflows/verify-package.yml +++ b/.github/workflows/verify-package.yml @@ -11,7 +11,11 @@ on: type: string description: The id of the create-release-artifacts workflow to download artifacts from required: true - + workflow_run: + workflows: ["Create Release Artifacts"] + types: + - completed + branches: [main] env: DOCKER_CMAKE_FLAGS: -DDOCKER_VERIFY_THREAD=3 -DUSE_SHARED_LIBS= -DSTRICT_GSL_CHECKS=AUDIT -DCI_BUILD=ON -DENABLE_AWS=ON -DENABLE_KAFKA=ON -DENABLE_MQTT=ON -DENABLE_AZURE=ON -DENABLE_SQL=ON \ @@ -19,8 +23,20 @@ env: -DENABLE_ELASTICSEARCH=OFF -DENABLE_GRAFANA_LOKI=ON -DENABLE_COUCHBASE=ON -DDOCKER_BUILD_ONLY=ON jobs: + check-artifacts-workflow: + name: "Check Create Release Artifacts status" + if: github.event_name == 'workflow_run' + runs-on: ubuntu-24.04 + steps: + - name: Check workflow conclusion + if: github.event.workflow_run.conclusion != 'success' + run: | + echo "Create Release Artifacts workflow failed with conclusion: ${{ github.event.workflow_run.conclusion }}" + exit 1 docker-test-modular: - name: "${{ matrix.platform.name }} (${{ matrix.arch }}) Modular${{ inputs.enable_fips && ' (FIPS Mode)' || '' }}" + name: "${{ matrix.platform.name }} (${{ matrix.arch }})${{ (github.event_name != 'workflow_dispatch' || inputs.enable_fips) && ' (FIPS Mode)' || '' }}" + needs: [check-artifacts-workflow] + if: ${{ !failure() && !cancelled() }} runs-on: ${{ matrix.arch == 'x86_64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} timeout-minutes: 240 strategy: @@ -45,14 +61,14 @@ jobs: - uses: actions/download-artifact@v4 with: - run-id: ${{ inputs.artifacts_workflow_id }} + run-id: ${{ inputs.artifacts_workflow_id || github.event.workflow_run.id }} name: minifi-${{ matrix.arch }}-tar path: build github-token: ${{ github.token }} - uses: actions/download-artifact@v4 with: - run-id: ${{ inputs.artifacts_workflow_id }} + run-id: ${{ inputs.artifacts_workflow_id || github.event.workflow_run.id }} name: minifi-${{ matrix.arch }}-rpm path: build github-token: ${{ github.token }} @@ -74,7 +90,7 @@ jobs: if: always() uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # v15 with: - name: "${{ matrix.platform.name }} (${{ matrix.arch }})${{ inputs.enable_fips && ' (FIPS Mode)' || '' }}" + name: "${{ matrix.platform.name }} (${{ matrix.arch }})${{ (github.event_name != 'workflow_dispatch' || inputs.enable_fips) && ' (FIPS Mode)' || '' }}" path: build/behavex_output_modular/behave/*.xml reporter: java-junit output-to: 'step-summary' @@ -85,5 +101,5 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: ${{ matrix.platform.id }}_${{ matrix.arch }}_behavex_output_modular${{ inputs.enable_fips && '_fips' || '' }} + name: ${{ matrix.platform.id }}_${{ matrix.arch }}_behavex_output_modular${{ (github.event_name != 'workflow_dispatch' || inputs.enable_fips) && '_fips' || '' }} path: build/behavex_output_modular diff --git a/README.md b/README.md index 909742536..9f816cfb2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ [<img src="https://nifi.apache.org/assets/images/minifi/minifi-logo.svg" width="300" height="126" alt="Apache NiFi MiNiFi"/>](https://nifi.apache.org/minifi/) -# Apache NiFi - MiNiFi - C++ [](https://github.com/apache/nifi-minifi-cpp/actions/workflows/ci.yml?query=workflow%3A%22MiNiFi-CPP+CI%22+branch%3Amain) +# Apache NiFi - MiNiFi - C++ +[](https://github.com/apache/nifi-minifi-cpp/actions/workflows/ci.yml?query=workflow%3A%22MiNiFi-CPP+CI%22+branch%3Amain)<br>[](https://github.com/apache/nifi-minifi-cpp/actions/workflows/compiler-support.yml?query=branch%3Amain)<br>[![MiNiFi-CPP Memchec [...] MiNiFi is a child project effort of Apache NiFi. This repository is for a native implementation in C++. diff --git a/docs/status/index.html b/docs/status/index.html new file mode 100644 index 000000000..70724a5e2 --- /dev/null +++ b/docs/status/index.html @@ -0,0 +1,661 @@ +<!-- This site was generated with the help of the generative AI model Claude Opus 4.6 --> +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>nifi-minifi-cpp — Status Page</title> +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet"> +<style> + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + + :root { + --bg: #f4f5f7; + --surface: #ffffff; + --border: #d8dae0; + --border-bright: #bfc2ca; + --text: #1a1a1e; + --muted: #6b7280; + --accent: #5b4fd6; + --green: #16a34a; + --green-dim: #dcfce7; + --red: #dc2626; + --red-dim: #fee2e2; + --yellow: #ca8a04; + --yellow-dim: #fef9c3; + --blue: #2563eb; + --blue-dim: #dbeafe; + } + + [data-theme="dark"] { + --bg: #0a0a0b; + --surface: #111114; + --border: #222228; + --border-bright: #333340; + --text: #e2e2e8; + --muted: #666672; + --accent: #7c6aff; + --green: #22c55e; + --green-dim: #15803d22; + --red: #ef4444; + --red-dim: #7f1d1d22; + --yellow: #eab308; + --yellow-dim: #71320a22; + --blue: #3b82f6; + --blue-dim: #1e3a5f22; + } + + html, body { + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: 'IBM Plex Sans', sans-serif; + font-size: 14px; + line-height: 1.5; + } + + body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(var(--border) 1px, transparent 1px), + linear-gradient(90deg, var(--border) 1px, transparent 1px); + background-size: 48px 48px; + opacity: 0.4; + pointer-events: none; + z-index: 0; + } + + .container { + position: relative; + z-index: 1; + max-width: 860px; + margin: 0 auto; + padding: 56px 24px 80px; + } + + .page-header { margin-bottom: 36px; } + + .repo-title { + font-family: 'IBM Plex Mono', monospace; + font-size: 22px; + font-weight: 600; + color: var(--text); + letter-spacing: -0.02em; + } + .repo-title a { color: inherit; text-decoration: none; } + .repo-title a:hover { color: var(--accent); } + .repo-title .slash { color: var(--muted); font-weight: 300; } + + .page-subtitle { + font-family: 'IBM Plex Mono', monospace; + font-size: 13px; + color: var(--muted); + margin-top: 6px; + letter-spacing: 0.05em; + } + + .page-meta { + display: flex; + align-items: center; + gap: 16px; + margin-top: 10px; + flex-wrap: wrap; + } + + .last-updated { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--muted); + } + + .overall-badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + font-weight: 500; + padding: 3px 10px; + border-radius: 20px; + letter-spacing: 0.05em; + } + .overall-badge.ok { background: var(--green-dim); color: var(--green); border: 1px solid #22c55e44; } + .overall-badge.degraded { background: var(--yellow-dim); color: var(--yellow); border: 1px solid #eab30844; } + .overall-badge.outage { background: var(--red-dim); color: var(--red); border: 1px solid #ef444444; } + .overall-badge.loading { background: var(--blue-dim); color: var(--blue); border: 1px solid #3b82f644; } + + .refresh-btn { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + background: transparent; + border: 1px solid var(--border-bright); + color: var(--muted); + border-radius: 5px; + padding: 3px 10px; + cursor: pointer; + transition: all 0.15s; + } + .refresh-btn:hover { color: var(--text); border-color: var(--muted); } + + .divider { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + } + .divider span { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--muted); + letter-spacing: 0.1em; + text-transform: uppercase; + white-space: nowrap; + } + .divider::before, .divider::after { + content: ''; + flex: 1; + height: 1px; + background: var(--border); + } + + .workflows-list { + display: flex; + flex-direction: column; + gap: 10px; + } + + .workflow-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 18px 22px; + display: flex; + align-items: center; + gap: 16px; + transition: border-color 0.2s, transform 0.15s; + animation: fadeUp 0.35s ease both; + } + .workflow-card:hover { + border-color: var(--border-bright); + transform: translateY(-1px); + } + + @keyframes fadeUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + + .status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + } + .status-dot.success { background: var(--green); box-shadow: 0 0 8px var(--green); } + .status-dot.failure { background: var(--red); box-shadow: 0 0 8px var(--red); } + .status-dot.cancelled { background: var(--muted); } + .status-dot.skipped { background: var(--muted); opacity: 0.5; } + .status-dot.in_progress, + .status-dot.queued, + .status-dot.waiting { background: var(--blue); box-shadow: 0 0 8px var(--blue); animation: pulse 1.5s infinite; } + .status-dot.unknown { background: var(--muted); } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } + } + + .workflow-info { flex: 1; min-width: 0; } + + .workflow-name { + font-family: 'IBM Plex Mono', monospace; + font-size: 14px; + font-weight: 500; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .workflow-sub { + display: flex; + gap: 14px; + margin-top: 4px; + flex-wrap: wrap; + } + .workflow-sub span, + .workflow-sub a { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--muted); + text-decoration: none; + } + .workflow-sub a:hover { color: var(--accent); } + .workflow-sub .err { color: var(--red) !important; } + + .history-bars { + display: flex; + gap: 2px; + align-items: flex-end; + flex-shrink: 0; + } + .bar { + width: 5px; + height: 20px; + border-radius: 2px; + background: var(--border-bright); + } + .bar.success { background: #bbf7d0; border: 1px solid #22c55e; } + .bar.failure { background: #fecaca; border: 1px solid #ef4444; } + .bar.in_progress, + .bar.queued { background: #bfdbfe; border: 1px solid #3b82f6; } + .bar.cancelled, + .bar.skipped { background: #d1d5db; } + + [data-theme="dark"] .bar.success { background: #22c55e55; border: 1px solid #22c55e88; } + [data-theme="dark"] .bar.failure { background: #ef444455; border: 1px solid #ef444488; } + [data-theme="dark"] .bar.in_progress, + [data-theme="dark"] .bar.queued { background: #3b82f655; border: 1px solid #3b82f688; } + [data-theme="dark"] .bar.cancelled, + [data-theme="dark"] .bar.skipped { background: #333340; } + + [data-theme="dark"] .overall-badge.ok { border-color: #22c55e44; } + [data-theme="dark"] .overall-badge.degraded { border-color: #eab30844; } + [data-theme="dark"] .overall-badge.outage { border-color: #ef444444; } + [data-theme="dark"] .overall-badge.loading { border-color: #3b82f644; } + + .status-label { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + font-weight: 500; + flex-shrink: 0; + text-align: right; + min-width: 80px; + } + .status-label.success { color: var(--green); } + .status-label.failure { color: var(--red); } + .status-label.in_progress, + .status-label.queued, + .status-label.waiting { color: var(--blue); } + .status-label.cancelled, + .status-label.skipped, + .status-label.unknown { color: var(--muted); } + + .state-msg { + text-align: center; + padding: 60px 24px; + font-family: 'IBM Plex Mono', monospace; + color: var(--muted); + font-size: 13px; + border: 1px dashed var(--border-bright); + border-radius: 8px; + line-height: 2; + } + .state-msg .icon { font-size: 32px; display: block; margin-bottom: 12px; } + + .footer-note { + text-align: center; + margin-top: 40px; + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--muted); + line-height: 2; + } + .footer-note a { color: var(--accent); text-decoration: none; } + .footer-note a:hover { text-decoration: underline; } + + .token-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + padding: 14px 18px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + flex-wrap: wrap; + } + .token-bar.hidden { display: none; } + .token-bar label { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + color: var(--muted); + white-space: nowrap; + } + .token-bar input { + flex: 1; + min-width: 180px; + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + background: var(--bg); + color: var(--text); + border: 1px solid var(--border-bright); + border-radius: 5px; + padding: 5px 10px; + outline: none; + transition: border-color 0.15s; + } + .token-bar input:focus { border-color: var(--accent); } + .token-bar input::placeholder { color: var(--muted); } + .token-bar button { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + background: transparent; + border: 1px solid var(--border-bright); + color: var(--muted); + border-radius: 5px; + padding: 5px 12px; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; + } + .token-bar button:hover { color: var(--text); border-color: var(--muted); } + .token-bar .token-status { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: var(--green); + } + .token-toggle { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + background: transparent; + border: 1px solid var(--border-bright); + color: var(--muted); + border-radius: 5px; + padding: 3px 10px; + cursor: pointer; + transition: all 0.15s; + } + .token-toggle:hover { color: var(--text); border-color: var(--muted); } + + .theme-toggle { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + background: transparent; + border: 1px solid var(--border-bright); + color: var(--muted); + border-radius: 5px; + padding: 3px 10px; + cursor: pointer; + transition: all 0.15s; + } + .theme-toggle:hover { color: var(--text); border-color: var(--muted); } + + @media (max-width: 600px) { + .history-bars { display: none; } + .status-label { min-width: unset; } + } +</style> +</head> +<body> +<div class="container"> + + <div class="page-header"> + <div class="repo-title"> + <a id="repoLink" href="#" target="_blank"> + <span id="repoOwner"></span><span class="slash"> / </span><span id="repoName"></span> + </a> + </div> + <div class="page-subtitle">Status Page</div> + <div class="page-meta"> + <span class="last-updated" id="lastUpdated">Loading…</span> + <span class="overall-badge loading" id="overallBadge">● LOADING</span> + <button class="refresh-btn" onclick="load()">↻ Refresh</button> + <button class="token-toggle" id="tokenToggle" onclick="toggleTokenBar()">🔑 Token</button> + <button class="theme-toggle" id="themeToggle" onclick="toggleTheme()">🌙 Dark</button> + </div> + </div> + + <div class="token-bar hidden" id="tokenBar"> + <label for="tokenInput">GitHub PAT:</label> + <input type="password" id="tokenInput" placeholder="ghp_... (stored in localStorage)" /> + <button onclick="saveToken()">Save</button> + <button onclick="clearToken()">Clear</button> + <span class="token-status" id="tokenStatus"></span> + <span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);width:100%;margin-top:4px;"> + Unauthenticated GitHub API requests are limited to 60/hr. Adding a <a href="https://github.com/settings/tokens" target="_blank" style="color:var(--accent);">personal access token</a> (no scopes needed for public repos) raises the limit to 5,000/hr. The token is only stored in your browser's localStorage. + </span> + </div> + + <div class="divider"><span>Workflows</span></div> + + <div class="workflows-list" id="workflowsList"> + <div class="state-msg"><span class="icon">⏳</span>Fetching workflow status…</div> + </div> + + <div class="footer-note"> + Click ↻ Refresh to update · + History bars show last 10 runs · + Data from <a href="https://docs.github.com/en/rest/actions/workflow-runs" target="_blank">GitHub Actions API</a> + </div> + +</div> +<script> + function detectRepo() { + const hostname = window.location.hostname; + const parts = hostname.split('.'); + if (parts.length >= 3 && parts[1] === 'github' && parts[2] === 'io') { + const owner = parts[0]; + const repo = window.location.pathname.split('/').filter(Boolean)[0] || ''; + if (owner && repo) return { owner, repo }; + } + throw new Error('Unable to detect GitHub repo from URL. Make sure this page is hosted on GitHub Pages with a URL like https://OWNER.github.io/REPO/'); + } + + const { owner: OWNER, repo: REPO } = detectRepo(); + + document.addEventListener('DOMContentLoaded', () => { + document.getElementById('repoOwner').textContent = OWNER; + document.getElementById('repoName').textContent = REPO; + document.getElementById('repoLink').href = `https://github.com/${OWNER}/${REPO}`; + }); + + + const CONFIG = { + owner: OWNER, + repo: REPO, + branch: 'main', + workflows: [ + 'ci.yml', + 'memcheck_ci.yml', + 'compiler-support.yml', + 'create-release-artifacts.yml', + 'verify-package.yml' + ], + token: localStorage.getItem('gh_status_token') || '' + }; + + + function getHeaders() { + const h = { 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' }; + if (CONFIG.token) h['Authorization'] = `Bearer ${CONFIG.token}`; + return h; + } + + async function fetchWorkflowName(file) { + try { + const r = await fetch( + `https://api.github.com/repos/${CONFIG.owner}/${CONFIG.repo}/actions/workflows/${file}`, + { headers: getHeaders() } + ); + if (!r.ok) return null; + const data = await r.json(); + return data.name || null; + } catch { return null; } + } + + async function fetchWorkflows() { + const { owner, repo, workflows } = CONFIG; + const results = []; + for (const file of workflows) { + try { + const r = await fetch( + `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${file}/runs?per_page=10&branch=${CONFIG.branch}`, + { headers: getHeaders() } + ); + if (!r.ok) { + const e = await r.json().catch(() => ({})); + const name = await fetchWorkflowName(file); + results.push({ file, name, error: e.message || `HTTP ${r.status}` }); + continue; + } + const data = await r.json(); + const runs = data.workflow_runs || []; + if (runs.length === 0) { + const name = await fetchWorkflowName(file); + results.push({ file, name, error: 'No runs found' }); + continue; + } + const latest = runs[0]; + const name = await fetchWorkflowName(file) || latest.name; + results.push({ + file, + name, + status: latest.status, + conclusion: latest.conclusion, + html_url: latest.html_url, + head_branch: latest.head_branch, + run_number: latest.run_number, + updated_at: latest.updated_at, + history: runs.map(r => r.conclusion || r.status) + }); + } catch (err) { + results.push({ file, error: err.message }); + } + } + return results; + } + + function effectiveStatus(wf) { + if (wf.error) return 'unknown'; + if (wf.status === 'completed') return wf.conclusion || 'unknown'; + return wf.status || 'unknown'; + } + + const LABELS = { + success: 'PASSING', failure: 'FAILING', cancelled: 'CANCELLED', + skipped: 'SKIPPED', in_progress: 'RUNNING', queued: 'QUEUED', + waiting: 'WAITING', unknown: 'UNKNOWN' + }; + + function timeAgo(iso) { + const diff = Math.floor((Date.now() - new Date(iso)) / 1000); + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; + } + + function renderCard(wf, idx) { + const s = effectiveStatus(wf); + const bars = (wf.history || []).slice(0, 10).map(h => { + const c = h === 'completed' ? 'success' : (h || 'unknown'); + return `<div class="bar ${c}" title="${c}"></div>`; + }).join(''); + return ` + <div class="workflow-card" style="animation-delay:${idx * 0.06}s"> + <div class="status-dot ${s}"></div> + <div class="workflow-info"> + <div class="workflow-name">${wf.name || wf.file}</div> + <div class="workflow-sub"> + <span>${wf.file}</span> + ${wf.head_branch ? `<span>branch: ${wf.head_branch}</span>` : ''} + ${wf.run_number ? `<a href="${wf.html_url}" target="_blank">run #${wf.run_number}</a>` : ''} + ${wf.updated_at ? `<span>${timeAgo(wf.updated_at)}</span>` : ''} + ${wf.error ? `<span class="err">⚠ ${wf.error}</span>` : ''} + </div> + </div> + <div class="history-bars">${bars}</div> + <div class="status-label ${s}">${LABELS[s] || s.toUpperCase()}</div> + </div>`; + } + + async function load() { + document.getElementById('lastUpdated').textContent = 'Refreshing…'; + document.getElementById('overallBadge').className = 'overall-badge loading'; + document.getElementById('overallBadge').textContent = '● LOADING'; + + try { + const workflows = await fetchWorkflows(); + const statuses = workflows.map(effectiveStatus); + const hasFail = statuses.some(s => s === 'failure'); + const hasRunning = statuses.some(s => ['in_progress','queued','waiting'].includes(s)); + const allOk = statuses.every(s => s === 'success'); + + const badge = document.getElementById('overallBadge'); + if (hasFail) { badge.className = 'overall-badge outage'; badge.textContent = '● DEGRADED'; } + else if (hasRunning) { badge.className = 'overall-badge loading'; badge.textContent = '● RUNNING'; } + else if (allOk) { badge.className = 'overall-badge ok'; badge.textContent = '● ALL PASSING'; } + else { badge.className = 'overall-badge degraded'; badge.textContent = '● PARTIAL'; } + + document.getElementById('lastUpdated').textContent = `Updated ${new Date().toLocaleTimeString()}`; + document.getElementById('workflowsList').innerHTML = workflows.map(renderCard).join(''); + } catch (err) { + document.getElementById('workflowsList').innerHTML = + `<div class="state-msg"><span class="icon">⚠️</span>${err.message}<br>You may be hitting the GitHub API rate limit (60 req/hr unauthenticated).<br>Click the 🔑 Token button above to add a GitHub PAT.</div>`; + showTokenBar(); + document.getElementById('overallBadge').className = 'overall-badge outage'; + document.getElementById('overallBadge').textContent = '● ERROR'; + document.getElementById('lastUpdated').textContent = `Failed at ${new Date().toLocaleTimeString()}`; + } + + } + + function toggleTokenBar() { + document.getElementById('tokenBar').classList.toggle('hidden'); + } + + function showTokenBar() { + document.getElementById('tokenBar').classList.remove('hidden'); + } + + function saveToken() { + const val = document.getElementById('tokenInput').value.trim(); + if (!val) return; + localStorage.setItem('gh_status_token', val); + CONFIG.token = val; + document.getElementById('tokenInput').value = ''; + document.getElementById('tokenStatus').textContent = '✓ saved'; + setTimeout(() => { document.getElementById('tokenStatus').textContent = '● token set'; }, 2000); + load(); + } + + function clearToken() { + localStorage.removeItem('gh_status_token'); + CONFIG.token = ''; + document.getElementById('tokenInput').value = ''; + document.getElementById('tokenStatus').textContent = '✓ cleared'; + setTimeout(() => { document.getElementById('tokenStatus').textContent = ''; }, 2000); + } + + document.addEventListener('DOMContentLoaded', () => { + if (CONFIG.token) { + document.getElementById('tokenStatus').textContent = '● token set'; + } + }); + + function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + const btn = document.getElementById('themeToggle'); + btn.textContent = theme === 'light' ? '🌙 Dark' : '☀ Light'; + } + + function toggleTheme() { + const current = document.documentElement.getAttribute('data-theme') || 'light'; + const next = current === 'light' ? 'dark' : 'light'; + localStorage.setItem('status_theme', next); + applyTheme(next); + } + + (function initTheme() { + const saved = localStorage.getItem('status_theme') || 'light'; + applyTheme(saved); + })(); + + load(); +</script> +</body> +</html>
