Spaces:
Running
Running
Claude
commited on
Commit
·
c884704
1
Parent(s):
19d9809
feat(builder): single-page dashboard with registry push fields
Browse files
app.py
CHANGED
|
@@ -1096,6 +1096,34 @@ def render_logs_html() -> str:
|
|
| 1096 |
return "".join(f'<div class="log-line">{line}</div>' for line in logs[-60:])
|
| 1097 |
|
| 1098 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1099 |
@app.route("/")
|
| 1100 |
def index():
|
| 1101 |
return render_template_string(HTML_TEMPLATE,
|
|
@@ -1104,6 +1132,8 @@ def index():
|
|
| 1104 |
current_html=render_current_build_html(),
|
| 1105 |
history_html=render_history_html(),
|
| 1106 |
logs_html=render_logs_html(),
|
|
|
|
|
|
|
| 1107 |
)
|
| 1108 |
|
| 1109 |
|
|
@@ -1127,6 +1157,16 @@ def logs_partial():
|
|
| 1127 |
return render_logs_html()
|
| 1128 |
|
| 1129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
HTML_TEMPLATE = """
|
| 1131 |
<!DOCTYPE html>
|
| 1132 |
<html lang="en">
|
|
@@ -1157,107 +1197,51 @@ HTML_TEMPLATE = """
|
|
| 1157 |
.main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
| 1158 |
|
| 1159 |
/* Header */
|
| 1160 |
-
.header { margin-bottom: 2rem; }
|
| 1161 |
-
.header h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.025em; }
|
| 1162 |
-
.header-meta {
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
}
|
| 1169 |
-
.
|
| 1170 |
-
.
|
| 1171 |
|
| 1172 |
/* Stats */
|
| 1173 |
-
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom:
|
| 1174 |
-
.stat-card {
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
border-radius: 0.75rem;
|
| 1178 |
-
padding: 1.25rem;
|
| 1179 |
-
}
|
| 1180 |
-
.stat-label {
|
| 1181 |
-
font-size: 0.75rem;
|
| 1182 |
-
color: var(--text-muted);
|
| 1183 |
-
text-transform: uppercase;
|
| 1184 |
-
letter-spacing: 0.05em;
|
| 1185 |
-
margin-bottom: 0.5rem;
|
| 1186 |
-
}
|
| 1187 |
-
.stat-value {
|
| 1188 |
-
font-size: 1.75rem;
|
| 1189 |
-
font-weight: 600;
|
| 1190 |
-
font-variant-numeric: tabular-nums;
|
| 1191 |
-
}
|
| 1192 |
.stat-value.success { color: var(--green); }
|
| 1193 |
.stat-value.failed { color: var(--red); }
|
| 1194 |
|
| 1195 |
/* Panels */
|
| 1196 |
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
|
| 1197 |
-
.panel {
|
| 1198 |
-
background: var(--surface);
|
| 1199 |
-
border: 1px solid var(--border);
|
| 1200 |
-
border-radius: 0.75rem;
|
| 1201 |
-
overflow: hidden;
|
| 1202 |
-
}
|
| 1203 |
.panel.full { grid-column: span 2; }
|
| 1204 |
-
.panel-header {
|
| 1205 |
-
display: flex;
|
| 1206 |
-
justify-content: space-between;
|
| 1207 |
-
align-items: center;
|
| 1208 |
-
padding: 1rem 1.25rem;
|
| 1209 |
-
border-bottom: 1px solid var(--border);
|
| 1210 |
-
}
|
| 1211 |
.panel-title { font-size: 0.875rem; font-weight: 500; }
|
| 1212 |
.panel-body { padding: 1rem 1.25rem; max-height: 320px; overflow-y: auto; }
|
| 1213 |
|
| 1214 |
/* Buttons */
|
| 1215 |
-
.btn {
|
| 1216 |
-
background: var(--accent);
|
| 1217 |
-
color: var(--bg);
|
| 1218 |
-
border: none;
|
| 1219 |
-
padding: 0.5rem 1rem;
|
| 1220 |
-
border-radius: 0.5rem;
|
| 1221 |
-
font-size: 0.8125rem;
|
| 1222 |
-
font-weight: 500;
|
| 1223 |
-
cursor: pointer;
|
| 1224 |
-
transition: opacity 0.15s;
|
| 1225 |
-
}
|
| 1226 |
.btn:hover { opacity: 0.9; }
|
| 1227 |
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 1228 |
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.75rem; }
|
| 1229 |
.btn-danger { background: var(--red); }
|
| 1230 |
-
.btn-ghost {
|
| 1231 |
-
background: transparent;
|
| 1232 |
-
color: var(--text-muted);
|
| 1233 |
-
border: 1px solid var(--border);
|
| 1234 |
-
}
|
| 1235 |
-
.btn-ghost:hover { background: var(--border); color: var(--text); }
|
| 1236 |
|
| 1237 |
/* Dot */
|
| 1238 |
-
.dot {
|
| 1239 |
-
width: 8px;
|
| 1240 |
-
height: 8px;
|
| 1241 |
-
border-radius: 50%;
|
| 1242 |
-
flex-shrink: 0;
|
| 1243 |
-
}
|
| 1244 |
.dot.idle { background: var(--text-muted); }
|
| 1245 |
.dot.building { background: var(--accent); animation: pulse 2s infinite; }
|
| 1246 |
.dot.success { background: var(--green); }
|
| 1247 |
.dot.failed { background: var(--red); }
|
| 1248 |
-
@keyframes pulse {
|
| 1249 |
-
0%, 100% { opacity: 1; }
|
| 1250 |
-
50% { opacity: 0.5; }
|
| 1251 |
-
}
|
| 1252 |
|
| 1253 |
/* Build items */
|
| 1254 |
-
.build-item {
|
| 1255 |
-
display: flex;
|
| 1256 |
-
align-items: center;
|
| 1257 |
-
gap: 1rem;
|
| 1258 |
-
padding: 0.75rem 0;
|
| 1259 |
-
border-bottom: 1px solid var(--border);
|
| 1260 |
-
}
|
| 1261 |
.build-item:last-child { border-bottom: none; }
|
| 1262 |
.build-item.active { background: rgba(245, 158, 11, 0.05); margin: -0.5rem -0.25rem; padding: 0.75rem; border-radius: 0.5rem; }
|
| 1263 |
.build-status { display: flex; align-items: center; gap: 0.5rem; min-width: 100px; }
|
|
@@ -1267,93 +1251,57 @@ HTML_TEMPLATE = """
|
|
| 1267 |
.build-meta { display: flex; gap: 1rem; font-size: 0.75rem; color: var(--text-muted); margin-top: 0.125rem; }
|
| 1268 |
|
| 1269 |
/* Badge */
|
| 1270 |
-
.badge {
|
| 1271 |
-
font-size: 0.6875rem;
|
| 1272 |
-
font-weight: 600;
|
| 1273 |
-
text-transform: uppercase;
|
| 1274 |
-
letter-spacing: 0.05em;
|
| 1275 |
-
padding: 0.125rem 0.5rem;
|
| 1276 |
-
border-radius: 0.25rem;
|
| 1277 |
-
background: var(--border);
|
| 1278 |
-
color: var(--text-muted);
|
| 1279 |
-
}
|
| 1280 |
.badge.success { background: rgba(74, 222, 128, 0.15); color: var(--green); }
|
| 1281 |
.badge.failed { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
| 1282 |
|
| 1283 |
/* Form */
|
| 1284 |
-
.form-grid { display: grid; gap:
|
| 1285 |
-
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap:
|
| 1286 |
-
.form-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
text-transform: uppercase;
|
| 1291 |
-
letter-spacing: 0.05em;
|
| 1292 |
-
margin-bottom: 0.375rem;
|
| 1293 |
-
}
|
| 1294 |
-
.form-group input {
|
| 1295 |
-
width: 100%;
|
| 1296 |
-
padding: 0.625rem 0.75rem;
|
| 1297 |
-
background: var(--bg);
|
| 1298 |
-
border: 1px solid var(--border);
|
| 1299 |
-
border-radius: 0.5rem;
|
| 1300 |
-
color: var(--text);
|
| 1301 |
-
font-family: ui-monospace, monospace;
|
| 1302 |
-
font-size: 0.875rem;
|
| 1303 |
-
}
|
| 1304 |
-
.form-group input:focus {
|
| 1305 |
-
outline: none;
|
| 1306 |
-
border-color: var(--accent);
|
| 1307 |
-
}
|
| 1308 |
.form-group input::placeholder { color: var(--text-muted); }
|
|
|
|
|
|
|
|
|
|
| 1309 |
|
| 1310 |
/* Logs */
|
| 1311 |
-
.logs-panel {
|
| 1312 |
-
|
| 1313 |
-
border: 1px solid var(--border);
|
| 1314 |
-
border-radius: 0.75rem;
|
| 1315 |
-
overflow: hidden;
|
| 1316 |
-
}
|
| 1317 |
-
.logs-header {
|
| 1318 |
-
display: flex;
|
| 1319 |
-
justify-content: space-between;
|
| 1320 |
-
align-items: center;
|
| 1321 |
-
padding: 0.75rem 1rem;
|
| 1322 |
-
border-bottom: 1px solid var(--border);
|
| 1323 |
-
background: rgba(0,0,0,0.2);
|
| 1324 |
-
}
|
| 1325 |
.logs-title { font-size: 0.8125rem; font-weight: 500; color: var(--text-muted); }
|
| 1326 |
-
.logs {
|
| 1327 |
-
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
| 1328 |
-
font-size: 0.75rem;
|
| 1329 |
-
line-height: 1.5;
|
| 1330 |
-
padding: 1rem;
|
| 1331 |
-
max-height: 220px;
|
| 1332 |
-
overflow-y: auto;
|
| 1333 |
-
background: var(--bg);
|
| 1334 |
-
}
|
| 1335 |
.log-line { color: var(--text-muted); }
|
| 1336 |
.log-line:hover { color: var(--text); }
|
| 1337 |
|
| 1338 |
.empty { color: var(--text-muted); font-size: 0.875rem; padding: 1rem 0; text-align: center; }
|
| 1339 |
|
| 1340 |
@media (max-width: 768px) {
|
|
|
|
| 1341 |
.stats { grid-template-columns: repeat(2, 1fr); }
|
| 1342 |
.panels { grid-template-columns: 1fr; }
|
| 1343 |
.panel.full { grid-column: span 1; }
|
| 1344 |
-
.form-row { grid-template-columns: 1fr; }
|
|
|
|
| 1345 |
}
|
| 1346 |
</style>
|
| 1347 |
</head>
|
| 1348 |
<body>
|
| 1349 |
<div class="main">
|
| 1350 |
<div class="header">
|
| 1351 |
-
<
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1357 |
</div>
|
| 1358 |
</div>
|
| 1359 |
|
|
@@ -1386,30 +1334,64 @@ HTML_TEMPLATE = """
|
|
| 1386 |
</div>
|
| 1387 |
<div class="panel-body">
|
| 1388 |
<form hx-post="/api/build" hx-swap="none" class="form-grid">
|
| 1389 |
-
<div class="form-group">
|
| 1390 |
-
<label>Repository URL</label>
|
| 1391 |
-
<input type="text" name="repo_url" placeholder="https://github.com/owner/repo" required>
|
| 1392 |
-
</div>
|
| 1393 |
<div class="form-row">
|
| 1394 |
<div class="form-group">
|
| 1395 |
-
<label>
|
| 1396 |
-
<input type="text" name="
|
| 1397 |
</div>
|
| 1398 |
<div class="form-group">
|
| 1399 |
<label>Branch</label>
|
| 1400 |
<input type="text" name="branch" value="main">
|
| 1401 |
</div>
|
| 1402 |
</div>
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1407 |
</div>
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1411 |
</div>
|
| 1412 |
</div>
|
|
|
|
| 1413 |
<button type="submit" class="btn">Build & Push</button>
|
| 1414 |
</form>
|
| 1415 |
</div>
|
|
|
|
| 1096 |
return "".join(f'<div class="log-line">{line}</div>' for line in logs[-60:])
|
| 1097 |
|
| 1098 |
|
| 1099 |
+
def render_badge_html() -> str:
|
| 1100 |
+
"""Render inline badge."""
|
| 1101 |
+
history = state.get_history(1)
|
| 1102 |
+
if not history:
|
| 1103 |
+
status, color = "no builds", "#737373"
|
| 1104 |
+
elif history[0]["status"] == "success":
|
| 1105 |
+
status, color = "passing", "#4ade80"
|
| 1106 |
+
elif state.is_building:
|
| 1107 |
+
status, color = "building", "#f59e0b"
|
| 1108 |
+
else:
|
| 1109 |
+
status, color = "failing", "#f87171"
|
| 1110 |
+
return f'<span class="status-badge" style="background: {color}20; color: {color}; border: 1px solid {color}40;">{status}</span>'
|
| 1111 |
+
|
| 1112 |
+
|
| 1113 |
+
def render_metrics_html() -> str:
|
| 1114 |
+
"""Render metrics panel."""
|
| 1115 |
+
metrics = state.get_metrics()
|
| 1116 |
+
return f"""
|
| 1117 |
+
<div class="metrics-grid">
|
| 1118 |
+
<div class="metric"><span class="metric-label">Success</span><span class="metric-value" style="color: var(--green);">{metrics['builds_completed']}</span></div>
|
| 1119 |
+
<div class="metric"><span class="metric-label">Failed</span><span class="metric-value" style="color: var(--red);">{metrics['builds_failed']}</span></div>
|
| 1120 |
+
<div class="metric"><span class="metric-label">Total</span><span class="metric-value">{metrics['builds_total']}</span></div>
|
| 1121 |
+
<div class="metric"><span class="metric-label">Rate</span><span class="metric-value">{metrics['success_rate']*100:.0f}%</span></div>
|
| 1122 |
+
<div class="metric"><span class="metric-label">Avg Time</span><span class="metric-value">{metrics['avg_duration_seconds']:.1f}s</span></div>
|
| 1123 |
+
</div>
|
| 1124 |
+
"""
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
@app.route("/")
|
| 1128 |
def index():
|
| 1129 |
return render_template_string(HTML_TEMPLATE,
|
|
|
|
| 1132 |
current_html=render_current_build_html(),
|
| 1133 |
history_html=render_history_html(),
|
| 1134 |
logs_html=render_logs_html(),
|
| 1135 |
+
badge_html=render_badge_html(),
|
| 1136 |
+
metrics_html=render_metrics_html(),
|
| 1137 |
)
|
| 1138 |
|
| 1139 |
|
|
|
|
| 1157 |
return render_logs_html()
|
| 1158 |
|
| 1159 |
|
| 1160 |
+
@app.route("/badge-partial")
|
| 1161 |
+
def badge_partial():
|
| 1162 |
+
return render_badge_html()
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
@app.route("/metrics-partial")
|
| 1166 |
+
def metrics_partial():
|
| 1167 |
+
return render_metrics_html()
|
| 1168 |
+
|
| 1169 |
+
|
| 1170 |
HTML_TEMPLATE = """
|
| 1171 |
<!DOCTYPE html>
|
| 1172 |
<html lang="en">
|
|
|
|
| 1197 |
.main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
| 1198 |
|
| 1199 |
/* Header */
|
| 1200 |
+
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; }
|
| 1201 |
+
.header-left h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.025em; display: flex; align-items: center; gap: 0.75rem; }
|
| 1202 |
+
.header-meta { display: flex; gap: 1rem; margin-top: 0.5rem; font-size: 0.75rem; color: var(--text-muted); font-family: ui-monospace, monospace; }
|
| 1203 |
+
.status-badge { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.25rem 0.625rem; border-radius: 9999px; }
|
| 1204 |
+
|
| 1205 |
+
/* Metrics bar */
|
| 1206 |
+
.metrics-bar { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem 1rem; }
|
| 1207 |
+
.metrics-grid { display: flex; gap: 1.5rem; }
|
| 1208 |
+
.metric { display: flex; flex-direction: column; align-items: center; }
|
| 1209 |
+
.metric-label { font-size: 0.625rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
| 1210 |
+
.metric-value { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
|
| 1211 |
|
| 1212 |
/* Stats */
|
| 1213 |
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
| 1214 |
+
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.25rem; }
|
| 1215 |
+
.stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
| 1216 |
+
.stat-value { font-size: 1.75rem; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
.stat-value.success { color: var(--green); }
|
| 1218 |
.stat-value.failed { color: var(--red); }
|
| 1219 |
|
| 1220 |
/* Panels */
|
| 1221 |
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
|
| 1222 |
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem; overflow: hidden; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1223 |
.panel.full { grid-column: span 2; }
|
| 1224 |
+
.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1225 |
.panel-title { font-size: 0.875rem; font-weight: 500; }
|
| 1226 |
.panel-body { padding: 1rem 1.25rem; max-height: 320px; overflow-y: auto; }
|
| 1227 |
|
| 1228 |
/* Buttons */
|
| 1229 |
+
.btn { background: var(--accent); color: var(--bg); border: none; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.8125rem; font-weight: 500; cursor: pointer; transition: opacity 0.15s; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
.btn:hover { opacity: 0.9; }
|
| 1231 |
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 1232 |
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.75rem; }
|
| 1233 |
.btn-danger { background: var(--red); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
|
| 1235 |
/* Dot */
|
| 1236 |
+
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
.dot.idle { background: var(--text-muted); }
|
| 1238 |
.dot.building { background: var(--accent); animation: pulse 2s infinite; }
|
| 1239 |
.dot.success { background: var(--green); }
|
| 1240 |
.dot.failed { background: var(--red); }
|
| 1241 |
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
|
|
|
|
|
|
|
|
| 1242 |
|
| 1243 |
/* Build items */
|
| 1244 |
+
.build-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--border); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
.build-item:last-child { border-bottom: none; }
|
| 1246 |
.build-item.active { background: rgba(245, 158, 11, 0.05); margin: -0.5rem -0.25rem; padding: 0.75rem; border-radius: 0.5rem; }
|
| 1247 |
.build-status { display: flex; align-items: center; gap: 0.5rem; min-width: 100px; }
|
|
|
|
| 1251 |
.build-meta { display: flex; gap: 1rem; font-size: 0.75rem; color: var(--text-muted); margin-top: 0.125rem; }
|
| 1252 |
|
| 1253 |
/* Badge */
|
| 1254 |
+
.badge { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.125rem 0.5rem; border-radius: 0.25rem; background: var(--border); color: var(--text-muted); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
.badge.success { background: rgba(74, 222, 128, 0.15); color: var(--green); }
|
| 1256 |
.badge.failed { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
| 1257 |
|
| 1258 |
/* Form */
|
| 1259 |
+
.form-grid { display: grid; gap: 0.875rem; }
|
| 1260 |
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.875rem; }
|
| 1261 |
+
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.875rem; }
|
| 1262 |
+
.form-group label { display: block; font-size: 0.6875rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem; }
|
| 1263 |
+
.form-group input { width: 100%; padding: 0.5rem 0.625rem; background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; color: var(--text); font-family: ui-monospace, monospace; font-size: 0.8125rem; }
|
| 1264 |
+
.form-group input:focus { outline: none; border-color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1265 |
.form-group input::placeholder { color: var(--text-muted); }
|
| 1266 |
+
.form-hint { font-size: 0.625rem; color: var(--text-muted); margin-top: 0.25rem; }
|
| 1267 |
+
.form-section { border-top: 1px solid var(--border); padding-top: 0.875rem; margin-top: 0.5rem; }
|
| 1268 |
+
.form-section-title { font-size: 0.6875rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.75rem; }
|
| 1269 |
|
| 1270 |
/* Logs */
|
| 1271 |
+
.logs-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.75rem; overflow: hidden; }
|
| 1272 |
+
.logs-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1273 |
.logs-title { font-size: 0.8125rem; font-weight: 500; color: var(--text-muted); }
|
| 1274 |
+
.logs { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.75rem; line-height: 1.5; padding: 1rem; max-height: 220px; overflow-y: auto; background: var(--bg); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1275 |
.log-line { color: var(--text-muted); }
|
| 1276 |
.log-line:hover { color: var(--text); }
|
| 1277 |
|
| 1278 |
.empty { color: var(--text-muted); font-size: 0.875rem; padding: 1rem 0; text-align: center; }
|
| 1279 |
|
| 1280 |
@media (max-width: 768px) {
|
| 1281 |
+
.header { flex-direction: column; gap: 1rem; }
|
| 1282 |
.stats { grid-template-columns: repeat(2, 1fr); }
|
| 1283 |
.panels { grid-template-columns: 1fr; }
|
| 1284 |
.panel.full { grid-column: span 1; }
|
| 1285 |
+
.form-row, .form-row-3 { grid-template-columns: 1fr; }
|
| 1286 |
+
.metrics-grid { flex-wrap: wrap; }
|
| 1287 |
}
|
| 1288 |
</style>
|
| 1289 |
</head>
|
| 1290 |
<body>
|
| 1291 |
<div class="main">
|
| 1292 |
<div class="header">
|
| 1293 |
+
<div class="header-left">
|
| 1294 |
+
<h1>
|
| 1295 |
+
Builder
|
| 1296 |
+
<span id="badge" hx-get="/badge-partial" hx-trigger="every 5s" hx-swap="innerHTML">{{ badge_html | safe }}</span>
|
| 1297 |
+
</h1>
|
| 1298 |
+
<div class="header-meta">
|
| 1299 |
+
<span>{{ config.runner_id }}</span>
|
| 1300 |
+
<span>{{ config.registry_url }}</span>
|
| 1301 |
+
</div>
|
| 1302 |
+
</div>
|
| 1303 |
+
<div class="metrics-bar" id="metrics" hx-get="/metrics-partial" hx-trigger="every 5s" hx-swap="innerHTML">
|
| 1304 |
+
{{ metrics_html | safe }}
|
| 1305 |
</div>
|
| 1306 |
</div>
|
| 1307 |
|
|
|
|
| 1334 |
</div>
|
| 1335 |
<div class="panel-body">
|
| 1336 |
<form hx-post="/api/build" hx-swap="none" class="form-grid">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
<div class="form-row">
|
| 1338 |
<div class="form-group">
|
| 1339 |
+
<label>Repository URL</label>
|
| 1340 |
+
<input type="text" name="repo_url" placeholder="https://github.com/owner/repo" required>
|
| 1341 |
</div>
|
| 1342 |
<div class="form-group">
|
| 1343 |
<label>Branch</label>
|
| 1344 |
<input type="text" name="branch" value="main">
|
| 1345 |
</div>
|
| 1346 |
</div>
|
| 1347 |
+
|
| 1348 |
+
<div class="form-section">
|
| 1349 |
+
<div class="form-section-title">Push Destination</div>
|
| 1350 |
+
<div class="form-row-3">
|
| 1351 |
+
<div class="form-group">
|
| 1352 |
+
<label>Registry</label>
|
| 1353 |
+
<input type="text" name="registry_url" placeholder="ghcr.io" value="{{ config.registry_url }}">
|
| 1354 |
+
</div>
|
| 1355 |
+
<div class="form-group">
|
| 1356 |
+
<label>Image Name</label>
|
| 1357 |
+
<input type="text" name="image_name" placeholder="owner/repo" required>
|
| 1358 |
+
</div>
|
| 1359 |
+
<div class="form-group">
|
| 1360 |
+
<label>Tags</label>
|
| 1361 |
+
<input type="text" name="tags" value="latest" placeholder="latest,v1.0">
|
| 1362 |
+
</div>
|
| 1363 |
</div>
|
| 1364 |
+
</div>
|
| 1365 |
+
|
| 1366 |
+
<div class="form-section">
|
| 1367 |
+
<div class="form-section-title">Build Options</div>
|
| 1368 |
+
<div class="form-row-3">
|
| 1369 |
+
<div class="form-group">
|
| 1370 |
+
<label>Dockerfile</label>
|
| 1371 |
+
<input type="text" name="dockerfile" value="Dockerfile">
|
| 1372 |
+
</div>
|
| 1373 |
+
<div class="form-group">
|
| 1374 |
+
<label>Context Path</label>
|
| 1375 |
+
<input type="text" name="context_path" value="." placeholder=".">
|
| 1376 |
+
</div>
|
| 1377 |
+
<div class="form-group">
|
| 1378 |
+
<label>Platform</label>
|
| 1379 |
+
<input type="text" name="platform" placeholder="linux/amd64">
|
| 1380 |
+
</div>
|
| 1381 |
+
</div>
|
| 1382 |
+
<div class="form-row">
|
| 1383 |
+
<div class="form-group">
|
| 1384 |
+
<label>Build Args</label>
|
| 1385 |
+
<input type="text" name="build_args" placeholder="KEY=value,KEY2=value2">
|
| 1386 |
+
<div class="form-hint">Comma-separated key=value pairs</div>
|
| 1387 |
+
</div>
|
| 1388 |
+
<div class="form-group">
|
| 1389 |
+
<label>GitHub Token (for private repos)</label>
|
| 1390 |
+
<input type="password" name="github_token" placeholder="ghp_...">
|
| 1391 |
+
</div>
|
| 1392 |
</div>
|
| 1393 |
</div>
|
| 1394 |
+
|
| 1395 |
<button type="submit" class="btn">Build & Push</button>
|
| 1396 |
</form>
|
| 1397 |
</div>
|