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

feat(builder): landscape layout with logs sidebar, env var defaults

Browse files
Files changed (1) hide show
  1. app.py +145 -140
app.py CHANGED
@@ -147,12 +147,17 @@ class NotifyOn(Enum):
147
  class Config:
148
  """Application configuration from environment variables."""
149
 
150
- registry_url: str = field(default_factory=lambda: os.getenv("REGISTRY_URL", "ghcr.io"))
151
  registry_user: str = field(default_factory=lambda: os.getenv("REGISTRY_USER", ""))
152
  registry_password: str = field(default_factory=lambda: os.getenv("REGISTRY_PASSWORD", ""))
153
  github_token: str = field(default_factory=lambda: os.getenv("GITHUB_TOKEN", ""))
154
  webhook_secret: str = field(default_factory=lambda: os.getenv("WEBHOOK_SECRET", ""))
155
  default_image: str = field(default_factory=lambda: os.getenv("DEFAULT_IMAGE", ""))
 
 
 
 
 
156
  runner_id: str = field(default_factory=lambda: os.getenv("RUNNER_ID", str(uuid.uuid4())[:8]))
157
  build_timeout: int = field(default_factory=lambda: int(os.getenv("BUILD_TIMEOUT", "1800")))
158
  enable_cache: bool = field(default_factory=lambda: os.getenv("ENABLE_CACHE", "").lower() == "true")
@@ -1194,218 +1199,218 @@ HTML_TEMPLATE = """
1194
  color: var(--text);
1195
  min-height: 100vh;
1196
  }
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; }
1248
- .build-id { font-family: ui-monospace, monospace; font-size: 0.75rem; color: var(--text-muted); }
1249
  .build-info { flex: 1; min-width: 0; }
1250
- .build-image { font-size: 0.875rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
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
 
1308
- <div class="stats" id="stats" hx-get="/stats-partial" hx-trigger="every 3s" hx-swap="innerHTML">
1309
- {{ stats_html | safe }}
1310
- </div>
1311
-
1312
- <div class="panels">
1313
- <div class="panel">
1314
- <div class="panel-header">
1315
- <span class="panel-title">Current Build</span>
1316
- </div>
1317
- <div class="panel-body" id="current" hx-get="/current-partial" hx-trigger="every 2s" hx-swap="innerHTML">
1318
- {{ current_html | safe }}
1319
  </div>
1320
  </div>
1321
 
1322
- <div class="panel">
1323
- <div class="panel-header">
1324
- <span class="panel-title">Build History</span>
1325
- </div>
1326
- <div class="panel-body" id="history" hx-get="/history-partial" hx-trigger="every 5s" hx-swap="innerHTML">
1327
- {{ history_html | safe }}
1328
- </div>
1329
  </div>
1330
 
1331
- <div class="panel full">
1332
- <div class="panel-header">
1333
- <span class="panel-title">New Build</span>
 
 
 
 
 
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>
1398
  </div>
1399
  </div>
1400
-
1401
- <div class="logs-panel">
1402
- <div class="logs-header">
1403
- <span class="logs-title">Logs</span>
1404
- </div>
1405
- <div id="logs" class="logs" hx-get="/logs-partial" hx-trigger="every 2s" hx-swap="innerHTML">
1406
- {{ logs_html | safe }}
1407
- </div>
1408
- </div>
1409
  </div>
1410
  </body>
1411
  </html>
 
147
  class Config:
148
  """Application configuration from environment variables."""
149
 
150
+ registry_url: str = field(default_factory=lambda: os.getenv("REGISTRY_URL", "ghcr.io/drengskapur"))
151
  registry_user: str = field(default_factory=lambda: os.getenv("REGISTRY_USER", ""))
152
  registry_password: str = field(default_factory=lambda: os.getenv("REGISTRY_PASSWORD", ""))
153
  github_token: str = field(default_factory=lambda: os.getenv("GITHUB_TOKEN", ""))
154
  webhook_secret: str = field(default_factory=lambda: os.getenv("WEBHOOK_SECRET", ""))
155
  default_image: str = field(default_factory=lambda: os.getenv("DEFAULT_IMAGE", ""))
156
+ default_branch: str = field(default_factory=lambda: os.getenv("DEFAULT_BRANCH", "main"))
157
+ default_tags: str = field(default_factory=lambda: os.getenv("DEFAULT_TAGS", "latest"))
158
+ default_dockerfile: str = field(default_factory=lambda: os.getenv("DEFAULT_DOCKERFILE", "Dockerfile"))
159
+ default_context: str = field(default_factory=lambda: os.getenv("DEFAULT_CONTEXT", "."))
160
+ default_platform: str = field(default_factory=lambda: os.getenv("DEFAULT_PLATFORM", ""))
161
  runner_id: str = field(default_factory=lambda: os.getenv("RUNNER_ID", str(uuid.uuid4())[:8]))
162
  build_timeout: int = field(default_factory=lambda: int(os.getenv("BUILD_TIMEOUT", "1800")))
163
  enable_cache: bool = field(default_factory=lambda: os.getenv("ENABLE_CACHE", "").lower() == "true")
 
1199
  color: var(--text);
1200
  min-height: 100vh;
1201
  }
1202
+
1203
+ /* Landscape layout */
1204
+ .layout { display: grid; grid-template-columns: 340px 1fr; min-height: 100vh; }
1205
+
1206
+ /* Left sidebar - Logs */
1207
+ .sidebar {
1208
+ background: var(--surface);
1209
+ border-right: 1px solid var(--border);
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ }
1213
+ .sidebar-header {
1214
+ padding: 1rem 1.25rem;
1215
+ border-bottom: 1px solid var(--border);
1216
+ display: flex;
1217
+ align-items: center;
1218
+ justify-content: space-between;
1219
+ }
1220
+ .sidebar-title { font-size: 0.75rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
1221
+ .logs {
1222
+ flex: 1;
1223
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
1224
+ font-size: 0.6875rem;
1225
+ line-height: 1.6;
1226
+ padding: 0.75rem 1rem;
1227
+ overflow-y: auto;
1228
+ background: var(--bg);
1229
+ }
1230
+ .log-line { color: var(--text-muted); white-space: pre-wrap; word-break: break-all; }
1231
+ .log-line:hover { color: var(--text); }
1232
+
1233
+ /* Main content */
1234
+ .main { padding: 1.5rem 2rem; overflow-y: auto; }
1235
 
1236
  /* Header */
1237
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
1238
+ .header-left h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.025em; display: flex; align-items: center; gap: 0.625rem; }
1239
+ .header-meta { display: flex; gap: 0.75rem; margin-top: 0.25rem; font-size: 0.6875rem; color: var(--text-muted); font-family: ui-monospace, monospace; }
1240
+ .status-badge { font-size: 0.625rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.1875rem 0.5rem; border-radius: 9999px; }
1241
+
1242
+ /* Stats row */
1243
+ .stats { display: flex; gap: 0.75rem; margin-bottom: 1.25rem; }
1244
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem 1rem; flex: 1; }
1245
+ .stat-label { font-size: 0.625rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem; }
1246
+ .stat-value { font-size: 1.25rem; font-weight: 600; font-variant-numeric: tabular-nums; }
 
 
 
 
 
 
 
1247
  .stat-value.success { color: var(--green); }
1248
  .stat-value.failed { color: var(--red); }
1249
 
1250
+ /* Panels grid */
1251
+ .panels { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem; }
1252
+ .panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
1253
  .panel.full { grid-column: span 2; }
1254
+ .panel-header { padding: 0.625rem 0.875rem; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2); }
1255
+ .panel-title { font-size: 0.6875rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
1256
+ .panel-body { padding: 0.75rem 0.875rem; max-height: 180px; overflow-y: auto; }
1257
 
1258
  /* Buttons */
1259
+ .btn { background: var(--accent); color: var(--bg); border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: opacity 0.15s; }
1260
  .btn:hover { opacity: 0.9; }
1261
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
1262
+ .btn-sm { padding: 0.1875rem 0.5rem; font-size: 0.625rem; }
1263
  .btn-danger { background: var(--red); }
1264
 
1265
  /* Dot */
1266
+ .dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
1267
  .dot.idle { background: var(--text-muted); }
1268
  .dot.building { background: var(--accent); animation: pulse 2s infinite; }
 
 
1269
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
1270
 
1271
  /* Build items */
1272
+ .build-item { display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
1273
  .build-item:last-child { border-bottom: none; }
1274
+ .build-item.active { background: rgba(245, 158, 11, 0.05); margin: -0.25rem -0.375rem; padding: 0.5rem 0.375rem; border-radius: 0.375rem; }
1275
+ .build-status { display: flex; align-items: center; gap: 0.375rem; }
1276
+ .build-id { font-family: ui-monospace, monospace; font-size: 0.625rem; color: var(--text-muted); }
1277
  .build-info { flex: 1; min-width: 0; }
1278
+ .build-image { font-size: 0.75rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1279
+ .build-meta { display: flex; gap: 0.625rem; font-size: 0.625rem; color: var(--text-muted); }
1280
 
1281
  /* Badge */
1282
+ .badge { font-size: 0.5625rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.0625rem 0.375rem; border-radius: 0.1875rem; background: var(--border); color: var(--text-muted); }
1283
  .badge.success { background: rgba(74, 222, 128, 0.15); color: var(--green); }
1284
  .badge.failed { background: rgba(248, 113, 113, 0.15); color: var(--red); }
1285
 
1286
  /* Form */
1287
+ .form-grid { display: grid; gap: 0.625rem; }
1288
+ .form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.625rem; }
1289
+ .form-row-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.625rem; }
1290
+ .form-row-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.625rem; }
1291
+ .form-group label { display: block; font-size: 0.5625rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.1875rem; }
1292
+ .form-group input { width: 100%; padding: 0.375rem 0.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 0.25rem; color: var(--text); font-family: ui-monospace, monospace; font-size: 0.6875rem; }
1293
  .form-group input:focus { outline: none; border-color: var(--accent); }
1294
  .form-group input::placeholder { color: var(--text-muted); }
1295
+ .form-actions { display: flex; justify-content: flex-end; margin-top: 0.25rem; }
 
 
 
 
 
 
 
 
 
 
1296
 
1297
+ .empty { color: var(--text-muted); font-size: 0.75rem; padding: 0.75rem 0; text-align: center; }
1298
 
1299
+ @media (max-width: 1024px) {
1300
+ .layout { grid-template-columns: 1fr; }
1301
+ .sidebar { display: none; }
1302
  .panels { grid-template-columns: 1fr; }
1303
  .panel.full { grid-column: span 1; }
1304
+ .form-row-3, .form-row-4 { grid-template-columns: 1fr 1fr; }
 
1305
  }
1306
  </style>
1307
  </head>
1308
  <body>
1309
+ <div class="layout">
1310
+ <div class="sidebar">
1311
+ <div class="sidebar-header">
1312
+ <span class="sidebar-title">Logs</span>
1313
+ <span id="badge" hx-get="/badge-partial" hx-trigger="every 5s" hx-swap="innerHTML">{{ badge_html | safe }}</span>
 
 
 
 
 
 
1314
  </div>
1315
+ <div id="logs" class="logs" hx-get="/logs-partial" hx-trigger="every 2s" hx-swap="innerHTML">
1316
+ {{ logs_html | safe }}
1317
  </div>
1318
  </div>
1319
 
1320
+ <div class="main">
1321
+ <div class="header">
1322
+ <div class="header-left">
1323
+ <h1>Builder</h1>
1324
+ <div class="header-meta">
1325
+ <span>{{ config.runner_id }}</span>
1326
+ <span>{{ config.registry_url }}</span>
1327
+ </div>
 
 
 
1328
  </div>
1329
  </div>
1330
 
1331
+ <div class="stats" id="stats" hx-get="/stats-partial" hx-trigger="every 3s" hx-swap="innerHTML">
1332
+ {{ stats_html | safe }}
 
 
 
 
 
1333
  </div>
1334
 
1335
+ <div class="panels">
1336
+ <div class="panel">
1337
+ <div class="panel-header">
1338
+ <span class="panel-title">Current Build</span>
1339
+ </div>
1340
+ <div class="panel-body" id="current" hx-get="/current-partial" hx-trigger="every 2s" hx-swap="innerHTML">
1341
+ {{ current_html | safe }}
1342
+ </div>
1343
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
1344
 
1345
+ <div class="panel">
1346
+ <div class="panel-header">
1347
+ <span class="panel-title">History</span>
1348
+ </div>
1349
+ <div class="panel-body" id="history" hx-get="/history-partial" hx-trigger="every 5s" hx-swap="innerHTML">
1350
+ {{ history_html | safe }}
1351
+ </div>
1352
+ </div>
1353
+
1354
+ <div class="panel full">
1355
+ <div class="panel-header">
1356
+ <span class="panel-title">New Build</span>
1357
+ </div>
1358
+ <div class="panel-body">
1359
+ <form hx-post="/api/build" hx-swap="none" class="form-grid">
1360
+ <div class="form-row-4">
1361
+ <div class="form-group" style="grid-column: span 2;">
1362
+ <label>Repository URL</label>
1363
+ <input type="text" name="repo_url" placeholder="https://github.com/owner/repo" required>
1364
+ </div>
1365
  <div class="form-group">
1366
+ <label>Branch</label>
1367
+ <input type="text" name="branch" value="{{ config.default_branch }}">
1368
  </div>
1369
  <div class="form-group">
1370
  <label>Image Name</label>
1371
+ <input type="text" name="image_name" value="{{ config.default_image }}" placeholder="owner/repo" required>
1372
+ </div>
1373
+ </div>
1374
+ <div class="form-row-4">
1375
+ <div class="form-group">
1376
+ <label>Registry</label>
1377
+ <input type="text" name="registry_url" value="{{ config.registry_url }}">
1378
  </div>
1379
  <div class="form-group">
1380
  <label>Tags</label>
1381
+ <input type="text" name="tags" value="{{ config.default_tags }}">
1382
  </div>
 
 
 
 
 
 
1383
  <div class="form-group">
1384
  <label>Dockerfile</label>
1385
+ <input type="text" name="dockerfile" value="{{ config.default_dockerfile }}">
1386
  </div>
1387
  <div class="form-group">
1388
+ <label>Context</label>
1389
+ <input type="text" name="context_path" value="{{ config.default_context }}">
1390
  </div>
1391
+ </div>
1392
+ <div class="form-row-4">
1393
  <div class="form-group">
1394
  <label>Platform</label>
1395
+ <input type="text" name="platform" value="{{ config.default_platform }}" placeholder="linux/amd64">
1396
  </div>
 
 
1397
  <div class="form-group">
1398
  <label>Build Args</label>
1399
+ <input type="text" name="build_args" placeholder="KEY=val,KEY2=val">
 
1400
  </div>
1401
  <div class="form-group">
1402
+ <label>GitHub Token</label>
1403
  <input type="password" name="github_token" placeholder="ghp_...">
1404
  </div>
1405
+ <div class="form-actions">
1406
+ <button type="submit" class="btn">Build & Push</button>
1407
+ </div>
1408
  </div>
1409
+ </form>
1410
+ </div>
 
 
1411
  </div>
1412
  </div>
1413
  </div>
 
 
 
 
 
 
 
 
 
1414
  </div>
1415
  </body>
1416
  </html>