Claude commited on
Commit
c884704
·
1 Parent(s): 19d9809

feat(builder): single-page dashboard with registry push fields

Browse files
Files changed (1) hide show
  1. app.py +136 -154
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
- display: flex;
1164
- gap: 1.5rem;
1165
- margin-top: 0.5rem;
1166
- font-size: 0.8125rem;
1167
- color: var(--text-muted);
1168
- }
1169
- .header-meta a { color: var(--accent); text-decoration: none; }
1170
- .header-meta a:hover { text-decoration: underline; }
1171
 
1172
  /* Stats */
1173
- .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem; }
1174
- .stat-card {
1175
- background: var(--surface);
1176
- border: 1px solid var(--border);
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: 1rem; }
1285
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
1286
- .form-group label {
1287
- display: block;
1288
- font-size: 0.75rem;
1289
- color: var(--text-muted);
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
- background: var(--surface);
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
- <h1>Builder</h1>
1352
- <div class="header-meta">
1353
- <span>{{ config.runner_id }}</span>
1354
- <span>{{ config.registry_url }}</span>
1355
- <a href="/badge">badge</a>
1356
- <a href="/api/metrics">metrics</a>
 
 
 
 
 
 
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>Image Name</label>
1396
- <input type="text" name="image_name" placeholder="owner/repo" required>
1397
  </div>
1398
  <div class="form-group">
1399
  <label>Branch</label>
1400
  <input type="text" name="branch" value="main">
1401
  </div>
1402
  </div>
1403
- <div class="form-row">
1404
- <div class="form-group">
1405
- <label>Tags (comma-separated)</label>
1406
- <input type="text" name="tags" value="latest">
 
 
 
 
 
 
 
 
 
 
 
 
1407
  </div>
1408
- <div class="form-group">
1409
- <label>Dockerfile</label>
1410
- <input type="text" name="dockerfile" value="Dockerfile">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>