jebin2's picture
Fix 400/422 errors: Correct payload structure, improve error logging, and update workflow inputs
c1a09cd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Workflow Runner</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f1f5f9;
--surface: #ffffff;
--primary: #4f46e5;
--primary-hover: #4338ca;
--text: #020617;
--text-secondary: #475569;
--border: #cbd5e1;
--input-bg: #f8fafc;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
padding: 40px 20px;
display: flex;
justify-content: center;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.container {
width: 100%;
max-width: 1200px;
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 10px;
}
h1 {
font-size: 24px;
font-weight: 700;
margin: 0;
letter-spacing: -0.02em;
}
.card {
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
input[type="text"],
input[type="number"],
input[type="password"],
select {
width: 100%;
padding: 12px 16px;
background-color: var(--input-bg);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text);
font-size: 14px;
box-sizing: border-box;
transition: all 0.2s;
}
input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.btn {
background-color: var(--primary);
border: none;
border-radius: 12px;
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
width: 100%;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
.btn:disabled {
background-color: var(--border);
color: var(--text-secondary);
cursor: not-allowed;
transform: none;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.env-vars-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
.status-badge {
padding: 4px 12px;
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.status-queued {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.status-in_progress {
background: rgba(99, 102, 241, 0.1);
color: var(--primary);
border: 1px solid rgba(99, 102, 241, 0.2);
}
.status-failure {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
border: 1px solid rgba(239, 68, 68, 0.2);
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 14px;
}
th {
text-align: left;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
td {
text-align: left;
padding: 16px;
border-bottom: 1px solid var(--border);
color: var(--text);
}
tr:last-child td {
border-bottom: none;
}
a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
a:hover {
color: var(--primary-hover);
text-decoration: underline;
}
.notification {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
border-radius: 12px;
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
transform: translateY(100px);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 100;
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
}
.notification.show {
transform: translateY(0);
}
.notification.success {
border-left: 4px solid var(--success);
color: var(--success);
}
.notification.error {
border-left: 4px solid var(--error);
color: var(--error);
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
justify-content: center;
align-items: center;
z-index: 200;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
max-width: 600px;
width: 90%;
max-height: 85vh;
display: flex;
flex-direction: column;
transform: scale(0.95);
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.modal-overlay.show .modal {
transform: scale(1);
}
.modal-header {
padding: 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 700;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 24px;
cursor: pointer;
padding: 4px;
line-height: 1;
border-radius: 8px;
transition: all 0.2s;
}
.modal-close:hover {
color: var(--text);
background-color: rgba(255, 255, 255, 0.05);
}
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 20px 24px;
border-top: 1px solid var(--border);
display: flex;
gap: 12px;
justify-content: flex-end;
background-color: rgba(2, 6, 23, 0.3);
border-radius: 0 0 20px 20px;
}
.btn-secondary {
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
width: auto;
}
.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.05);
color: var(--text);
border-color: var(--text-secondary);
}
/* Custom Styles for Input Toggles */
.workflow-radio-group {
display: flex;
gap: 12px;
background: var(--input-bg);
padding: 4px;
border: 1px solid var(--border);
border-radius: 12px;
width: fit-content;
}
.workflow-radio-option {
position: relative;
}
.workflow-radio-option input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.workflow-radio-option label {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 0;
cursor: pointer;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
text-transform: none;
letter-spacing: normal;
}
.workflow-radio-option input:checked+label {
background-color: var(--primary);
color: white;
font-weight: 600;
}
.config-section {
margin-bottom: 24px;
}
.config-section:last-child {
margin-bottom: 0;
}
.config-section-title {
font-size: 12px;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: 12px;
letter-spacing: 0.05em;
}
.config-item {
display: flex;
justify-content: space-between;
padding: 10px 14px;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
margin-bottom: 8px;
font-size: 14px;
border: 1px solid transparent;
}
.config-item:hover {
border-color: var(--border);
}
.config-key {
font-family: 'Monaco', 'Consolas', monospace;
color: var(--text-secondary);
}
.config-value {
color: var(--text);
font-family: 'Monaco', 'Consolas', monospace;
text-align: right;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-value.empty {
color: var(--text-secondary);
font-style: italic;
}
.env-vars-summary {
max-height: 250px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
background: var(--input-bg);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 GitHub Workflow Runner</h1>
<div style="display: flex; gap: 12px; align-items: center;">
<a href="/" class="btn btn-secondary"
style="width: auto; padding: 8px 16px; font-size: 14px; text-decoration: none; display: flex; align-items: center; gap: 8px; border-radius: 99px;">
<span>🏠</span> Back
</a>
<div id="authStatus" class="status-badge">Checking Auth...</div>
</div>
</div>
<!-- Auth Card (Hidden if authed) -->
<div id="authCard" class="card" style="display:none;">
<div class="section-title">
<span>🔐</span> Authentication Required
</div>
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 20px; line-height: 1.5;">
A GitHub Personal Access Token (PAT) with <code
style="background: rgba(255,255,255,0.1); padding: 2px 4px; border-radius: 4px;">workflow</code>
scope is
required to trigger actions.
</p>
<div class="form-group">
<label>GitHub Token</label>
<input type="password" id="tokenInput" placeholder="ghp_...">
</div>
<button class="btn" onclick="saveToken()">Save Token</button>
</div>
<!-- Runner Card -->
<div class="card">
<div class="section-title">
<span></span> Trigger Workflow
</div>
<div class="form-group" style="margin-bottom: 24px;">
<label style="margin-bottom: 12px;">Select Workflow</label>
<div class="workflow-radio-group">
<div class="workflow-radio-option">
<input type="radio" id="wf_process" name="workflow_select" value="process_csv.yml" checked
onchange="toggleWorkflowInputs()">
<label for="wf_process">Process CSV</label>
</div>
<div class="workflow-radio-option">
<input type="radio" id="wf_publisher" name="workflow_select" value="publisher.yml"
onchange="toggleWorkflowInputs()">
<label for="wf_publisher">Social Publisher</label>
</div>
</div>
</div>
<hr style="border: 0; border-top: 1px solid var(--border); margin: 24px 0;">
<!-- Process CSV Inputs (Consolidated into Environment Variables) -->
<div id="processCsvInputs"></div>
<!-- Publisher Inputs (Consolidated into Environment Variables) -->
<div id="publisherInputs" style="display: none;"></div>
<div class="form-group" style="margin-top: 24px;">
<label>Branch / Ref</label>
<input type="text" id="refInput" value="feature/video-revamp" style="font-family: monospace;">
</div>
<!-- Env Vars (Only for Process CSV) -->
<div id="envVarsSection">
<div class="section-title" style="margin-top: 24px; border: none; padding: 0;">Environment Variables
</div>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: -10px; margin-bottom: 16px;">
Select variables to include in the run.
</p>
<button class="btn btn-secondary" onclick="openBulkModal()"
style="width: auto; margin-bottom: 16px; font-size: 13px; padding: 8px 16px;">
📋 Bulk Paste .env
</button>
<div id="envVarsContainer" class="env-vars-container grid-3">
<!-- Dynamic Content -->
<div style="padding: 40px; text-align: center; color: var(--text-secondary); grid-column: 1 / -1;">
<div class="animate-pulse">Loading variables...</div>
</div>
</div>
</div>
<div style="margin-top: 24px; display: flex; justify-content: flex-end;">
<button id="runBtn" class="btn" onclick="triggerWorkflow()"
style="width: auto; padding-left: 32px; padding-right: 32px;">
Run Workflow
</button>
</div>
</div>
<!-- Recent Runs -->
<div class="card">
<div class="section-title">Recent Runs</div>
<div style="overflow-x: auto;">
<table id="runsTable">
<thead>
<tr>
<th>ID</th>
<th>Workflow</th>
<th>Status</th>
<th>Created</th>
<th>Link</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5" style="text-align: center; color: var(--text-secondary);">Loading...</td>
</tr>
</tbody>
</table>
</div>
<button class="btn" onclick="fetchRuns()"
style="margin-top: 15px; background: transparent; border: 1px solid var(--border);">Refresh
List</button>
</div>
</div>
<div id="notification" class="notification"></div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2>⚡ Confirm Workflow Run</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div id="confirmContent">
<!-- Dynamic content will be inserted here -->
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button id="confirmRunBtn" class="btn" onclick="confirmAndRun()">🚀 Confirm & Run</button>
</div>
</div>
</div>
<div id="bulkEnvModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2>📋 Bulk Paste Environment Variables</h2>
<button class="modal-close" onclick="closeBulkModal()">&times;</button>
</div>
<div class="modal-body">
<p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 12px;">
Paste your .env file content below. Lines starting with # are ignored.
Applying will check only the variables present below.
</p>
<textarea id="bulkEnvInput"
style="width: 94%; height: 300px; padding: 16px; background: var(--input-bg); border: 1px solid var(--border); border-radius: 12px; color: var(--text); font-family: monospace; font-size: 13px; resize: vertical;"
placeholder="KEY=VALUE&#10;# Comment&#10;ANOTHER_KEY=123"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeBulkModal()">Cancel</button>
<button class="btn" onclick="processBulkEnv()">✨ Apply Variables</button>
</div>
</div>
</div>
<script>
let githubToken = localStorage.getItem('github_token') || '';
async function init() {
await checkAuth();
// Start with defaults
await toggleWorkflowInputs();
fetchRuns();
setInterval(fetchRuns, 10000); // Poll every 10s
}
async function checkAuth() {
// First check backend (env var)
try {
const res = await fetch('api/auth/status');
const data = await res.json();
if (data.authenticated) {
document.getElementById('authStatus').textContent = `Logged in as ${data.user}`;
document.getElementById('authStatus').className = 'status-badge status-success';
document.getElementById('authCard').style.display = 'none';
} else {
// Check local storage override
if (githubToken) {
document.getElementById('authStatus').textContent = 'Using Local Token';
document.getElementById('authStatus').className = 'status-badge status-queued';
document.getElementById('authCard').style.display = 'none';
} else {
document.getElementById('authStatus').textContent = 'Not Authenticated';
document.getElementById('authStatus').className = 'status-badge status-failure';
document.getElementById('authCard').style.display = 'block';
}
}
} catch (e) {
console.error(e);
}
}
function saveToken() {
const token = document.getElementById('tokenInput').value.trim();
if (token) {
localStorage.setItem('github_token', token);
githubToken = token;
checkAuth();
fetchRuns();
}
}
async function toggleWorkflowInputs() {
const isProcess = document.getElementById('wf_process').checked;
const workflow = isProcess ? 'process_csv.yml' : 'publisher.yml';
// Fetch vars for this workflow (rebuilds the env var list)
await fetchEnvVars(workflow);
// Refresh runs list to show relevant runs for selected workflow
fetchRuns();
}
async function fetchEnvVars(workflowName) {
try {
// If not provided, try to find from DOM
if (!workflowName) {
const isProcess = document.getElementById('wf_process').checked;
workflowName = isProcess ? 'process_csv.yml' : 'publisher.yml';
}
const res = await fetch(`api/env-vars?workflow=${workflowName}`);
const data = await res.json();
const vars = data.vars || {};
const container = document.getElementById('envVarsContainer');
container.innerHTML = '';
// Use keys returned by API as source of truth
const allKeys = new Set(Object.keys(vars));
const sortedKeys = Array.from(allKeys).sort();
sortedKeys.forEach(key => {
const val = vars[key] || '';
const div = document.createElement('div');
div.className = 'form-group';
div.style.background = 'rgba(255,255,255,0.03)';
div.style.padding = '8px';
div.style.borderRadius = '4px';
// Checkbox ID
const checkId = `check_${key}`;
div.innerHTML = `
<div style="display: flex; align-items: center; margin-bottom: 5px; gap: 8px;">
<input type="checkbox" id="${checkId}" class="env-check" data-key="${key}" style="width: auto;">
<label for="${checkId}" style="margin: 0; font-size: 12px; font-family: monospace; cursor: pointer; user-select: none;">${key}</label>
</div>
<input type="text" class="env-input" data-key="${key}" value="${val}" disabled style="opacity: 0.5;">
`;
container.appendChild(div);
// Add event listener to toggle input
const checkbox = div.querySelector(`#${checkId}`);
const input = div.querySelector('.env-input');
checkbox.addEventListener('change', (e) => {
input.disabled = !e.target.checked;
input.style.opacity = e.target.checked ? '1' : '0.5';
if (e.target.checked) {
input.focus();
}
});
});
} catch (e) {
console.error('Failed to fetch vars', e);
}
}
// Store pending workflow data
let pendingWorkflowData = null;
function openBulkModal() {
const container = document.getElementById('envVarsContainer');
const checks = container.querySelectorAll('.env-check:checked');
let envContent = '';
checks.forEach(check => {
const key = check.getAttribute('data-key');
const input = container.querySelector(`.env-input[data-key="${key}"]`);
if (input) {
envContent += `${key}=${input.value}\n`;
}
});
document.getElementById('bulkEnvInput').value = envContent;
document.getElementById('bulkEnvModal').classList.add('show');
}
function closeBulkModal() {
document.getElementById('bulkEnvModal').classList.remove('show');
}
function processBulkEnv() {
const raw = document.getElementById('bulkEnvInput').value;
const lines = raw.split('\n');
const updates = {};
// Parse pasted content
lines.forEach(line => {
line = line.trim();
// Ignore comments and empty lines
if (!line || line.startsWith('#')) return;
const idx = line.indexOf('=');
if (idx !== -1) {
const key = line.substring(0, idx).trim();
let val = line.substring(idx + 1).trim();
// Remove surrounding double quotes if present
if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) {
val = val.substring(1, val.length - 1);
}
// Remove surrounding single quotes if present
else if (val.startsWith("'") && val.endsWith("'") && val.length >= 2) {
val = val.substring(1, val.length - 1);
}
updates[key] = val;
}
});
// Update UI
const container = document.getElementById('envVarsContainer');
const allChecks = container.querySelectorAll('.env-check');
// First uncheck everything
allChecks.forEach(check => {
check.checked = false;
const key = check.getAttribute('data-key');
const input = container.querySelector(`.env-input[data-key="${key}"]`);
if (input) {
input.disabled = true;
input.style.opacity = '0.5';
}
});
// Then apply updates
Object.keys(updates).forEach(key => {
const check = container.querySelector(`.env-check[data-key="${key}"]`);
const input = container.querySelector(`.env-input[data-key="${key}"]`);
if (check && input) {
check.checked = true;
input.disabled = false;
input.style.opacity = '1';
input.value = updates[key];
}
});
closeBulkModal();
// Show notification
const count = Object.keys(updates).length;
showNotification(`Applied ${count} variables`, 'success');
}
function showNotification(message, type = 'success') {
const notif = document.getElementById('notification');
notif.textContent = message;
notif.className = `notification ${type} show`;
setTimeout(() => {
notif.classList.remove('show');
}, 3000);
}
function triggerWorkflow() {
const workflow = document.querySelector('input[name="workflow_select"]:checked').value;
const ref = document.getElementById('refInput').value;
let inputs = {};
// Workflow specific inputs are now handled as generic inputs below
// if (workflow === 'process_csv.yml') { ... }
// Publisher inputs (UPLOAD_LIMIT, PLATFORMS) are now handled as generic envVars below
// Collect Env Vars (Common for both)
const envVars = {};
document.querySelectorAll('.env-check').forEach(checkbox => {
if (checkbox.checked) {
const key = checkbox.dataset.key;
const input = checkbox.closest('.form-group').querySelector('.env-input');
const val = input.value.trim();
envVars[key] = val;
inputs[key] = val;
}
});
// Store for confirmation
pendingWorkflowData = { inputs, ref, envVars, workflow };
// Show confirmation modal
showConfirmModal(inputs, ref, envVars, workflow);
}
function showConfirmModal(inputs, ref, envVars, workflow) {
const content = document.getElementById('confirmContent');
// Build confirmation content
let html = `
<div class="config-section">
<div class="config-section-title">Workflow Settings</div>
<div class="config-item">
<span class="config-key">Workflow</span>
<span class="config-value">${workflow}</span>
</div>
<div class="config-item">
<span class="config-key">Branch / Ref</span>
<span class="config-value">${ref}</span>
</div>
`;
if (workflow === 'process_csv.yml') {
html += `
<div class="config-item">
<span class="config-key">Job Index</span>
<span class="config-value">${inputs.JOB_INDEX}</span>
</div>
<div class="config-item">
<span class="config-key">Total Jobs</span>
<span class="config-value">${inputs.TOTAL_JOBS}</span>
</div>
`;
}
html += `</div>`;
// Inputs / Env Vars section
const envKeys = Object.keys(envVars);
if (envKeys.length > 0) {
const title = workflow === 'publisher.yml' ? 'Inputs' : 'Environment Variables';
html += `
<div class="config-section">
<div class="config-section-title">${title} (${envKeys.length})</div>
<div class="env-vars-summary">
`;
envKeys.forEach(key => {
const val = envVars[key];
const displayVal = val ? val : '<empty>';
const valueClass = val ? 'config-value' : 'config-value empty';
html += `
<div class="config-item">
<span class="config-key">${key}</span>
<span class="${valueClass}" title="${val}">${displayVal}</span>
</div>
`;
});
html += `
</div>
</div>
`;
} else {
// only show if process csv, otherwise it looks empty for publisher if no inputs
if (workflow === 'process_csv.yml') {
html += `
<div class="config-section">
<div class="config-section-title">Environment Variables</div>
<p style="color: var(--text-secondary); font-size: 14px; margin: 0;">
No environment variables selected.
</p>
</div>
`;
}
}
content.innerHTML = html;
document.getElementById('confirmModal').classList.add('show');
}
function closeModal() {
document.getElementById('confirmModal').classList.remove('show');
pendingWorkflowData = null;
}
async function confirmAndRun() {
if (!pendingWorkflowData) return;
const { inputs, ref, workflow } = pendingWorkflowData;
closeModal();
const btn = document.getElementById('runBtn');
btn.disabled = true;
btn.textContent = 'Triggering...';
try {
// Construct payload explicitly to ensure token is included and structure matches schema
const payload = {
token: githubToken,
inputs: inputs,
ref: ref,
workflow: workflow
};
const res = await fetch('api/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok) {
showNotification('Workflow triggered successfully! 🚀', 'success');
// Refresh runs after a short delay
setTimeout(fetchRuns, 2000);
} else {
let errorMessage = 'Failed to trigger';
if (data.detail) {
if (Array.isArray(data.detail)) {
// Handle Pydantic validation errors (list of objects)
errorMessage = data.detail.map(e => `${e.loc.join('.')}: ${e.msg}`).join(', ');
} else {
errorMessage = data.detail;
}
}
showNotification(`Error: ${errorMessage}`, 'error');
}
} catch (e) {
console.error(e);
showNotification('Failed to trigger workflow', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Run Workflow';
pendingWorkflowData = null;
}
}
async function fetchRuns() {
const workflow = document.querySelector('input[name="workflow_select"]:checked') ?
document.querySelector('input[name="workflow_select"]:checked').value : 'process_csv.yml';
try {
// Append token and workflow to query
let url = `api/runs?workflow=${workflow}`;
if (githubToken) {
url += `&token=${encodeURIComponent(githubToken)}`;
}
const res = await fetch(url);
const data = await res.json();
const tbody = document.querySelector('#runsTable tbody');
tbody.innerHTML = '';
if (data.workflow_runs && data.workflow_runs.length > 0) {
data.workflow_runs.forEach(run => {
const statusClass = `status-${run.status === 'completed' ? (run.conclusion || 'success') : run.status}`;
const date = new Date(run.created_at).toLocaleString();
const tr = document.createElement('tr');
tr.innerHTML = `
<td>#${run.run_number}</td>
<td>${run.name}</td>
<td><span class="status-badge ${statusClass}">${run.status}${run.conclusion ? ': ' + run.conclusion : ''}</span></td>
<td>${date}</td>
<td><a href="${run.html_url}" target="_blank">View Logs</a></td>
`;
tbody.appendChild(tr);
});
} else if (data.error) {
tbody.innerHTML = `<tr><td colspan="5" style="color: #da3633;">Error: ${data.error}</td></tr>`;
} else {
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No runs found for ${workflow}</td></tr>`;
}
} catch (e) {
console.error('Failed to fetch runs', e);
}
}
function showNotification(msg, type) {
const el = document.getElementById('notification');
el.textContent = msg;
el.className = `notification ${type} show`;
setTimeout(() => {
el.className = 'notification';
}, 5000);
}
// Start
init();
</script>
</body>
</html>