|
|
<!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; |
|
|
} |
|
|
|
|
|
|
|
|
::-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-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); |
|
|
} |
|
|
|
|
|
|
|
|
.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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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;"> |
|
|
|
|
|
|
|
|
<div id="processCsvInputs"></div> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<div id="confirmModal" class="modal-overlay"> |
|
|
<div class="modal"> |
|
|
<div class="modal-header"> |
|
|
<h2>⚡ Confirm Workflow Run</h2> |
|
|
<button class="modal-close" onclick="closeModal()">×</button> |
|
|
</div> |
|
|
<div class="modal-body"> |
|
|
<div id="confirmContent"> |
|
|
|
|
|
</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()">×</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 # Comment 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(); |
|
|
|
|
|
await toggleWorkflowInputs(); |
|
|
fetchRuns(); |
|
|
setInterval(fetchRuns, 10000); |
|
|
} |
|
|
|
|
|
async function checkAuth() { |
|
|
|
|
|
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 { |
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
await fetchEnvVars(workflow); |
|
|
|
|
|
|
|
|
fetchRuns(); |
|
|
} |
|
|
|
|
|
async function fetchEnvVars(workflowName) { |
|
|
try { |
|
|
|
|
|
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 = ''; |
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 = {}; |
|
|
|
|
|
|
|
|
lines.forEach(line => { |
|
|
line = line.trim(); |
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) { |
|
|
val = val.substring(1, val.length - 1); |
|
|
} |
|
|
|
|
|
else if (val.startsWith("'") && val.endsWith("'") && val.length >= 2) { |
|
|
val = val.substring(1, val.length - 1); |
|
|
} |
|
|
|
|
|
updates[key] = val; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const container = document.getElementById('envVarsContainer'); |
|
|
const allChecks = container.querySelectorAll('.env-check'); |
|
|
|
|
|
|
|
|
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'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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 = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
pendingWorkflowData = { inputs, ref, envVars, workflow }; |
|
|
|
|
|
|
|
|
showConfirmModal(inputs, ref, envVars, workflow); |
|
|
} |
|
|
|
|
|
function showConfirmModal(inputs, ref, envVars, workflow) { |
|
|
const content = document.getElementById('confirmContent'); |
|
|
|
|
|
|
|
|
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>`; |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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 { |
|
|
|
|
|
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'); |
|
|
|
|
|
setTimeout(fetchRuns, 2000); |
|
|
} else { |
|
|
let errorMessage = 'Failed to trigger'; |
|
|
if (data.detail) { |
|
|
if (Array.isArray(data.detail)) { |
|
|
|
|
|
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 { |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
init(); |
|
|
|
|
|
</script> |
|
|
|
|
|
</body> |
|
|
|
|
|
</html> |