Spaces:
Sleeping
Sleeping
Claude
commited on
Commit
·
aea95ac
1
Parent(s):
c884704
feat(builder): landscape layout with logs sidebar, env var defaults
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
|
| 1199 |
/* Header */
|
| 1200 |
-
.header { display: flex; justify-content: space-between; align-items:
|
| 1201 |
-
.header-left h1 { font-size: 1.
|
| 1202 |
-
.header-meta { display: flex; gap:
|
| 1203 |
-
.status-badge { font-size: 0.
|
| 1204 |
-
|
| 1205 |
-
/*
|
| 1206 |
-
.
|
| 1207 |
-
.
|
| 1208 |
-
.
|
| 1209 |
-
.
|
| 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:
|
| 1222 |
-
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.
|
| 1223 |
.panel.full { grid-column: span 2; }
|
| 1224 |
-
.panel-header {
|
| 1225 |
-
.panel-title { font-size: 0.
|
| 1226 |
-
.panel-body { padding:
|
| 1227 |
|
| 1228 |
/* Buttons */
|
| 1229 |
-
.btn { background: var(--accent); color: var(--bg); border: none; padding: 0.5rem 1rem; border-radius: 0.
|
| 1230 |
.btn:hover { opacity: 0.9; }
|
| 1231 |
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 1232 |
-
.btn-sm { padding: 0.
|
| 1233 |
.btn-danger { background: var(--red); }
|
| 1234 |
|
| 1235 |
/* Dot */
|
| 1236 |
-
.dot { width:
|
| 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:
|
| 1245 |
.build-item:last-child { border-bottom: none; }
|
| 1246 |
-
.build-item.active { background: rgba(245, 158, 11, 0.05); margin: -0.
|
| 1247 |
-
.build-status { display: flex; align-items: center; gap: 0.
|
| 1248 |
-
.build-id { font-family: ui-monospace, monospace; font-size: 0.
|
| 1249 |
.build-info { flex: 1; min-width: 0; }
|
| 1250 |
-
.build-image { font-size: 0.
|
| 1251 |
-
.build-meta { display: flex; gap:
|
| 1252 |
|
| 1253 |
/* Badge */
|
| 1254 |
-
.badge { font-size: 0.
|
| 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.
|
| 1260 |
-
.form-row { display: grid; grid-template-columns:
|
| 1261 |
-
.form-row-3 { display: grid; grid-template-columns:
|
| 1262 |
-
.form-
|
| 1263 |
-
.form-group
|
|
|
|
| 1264 |
.form-group input:focus { outline: none; border-color: var(--accent); }
|
| 1265 |
.form-group input::placeholder { color: var(--text-muted); }
|
| 1266 |
-
.form-
|
| 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.
|
| 1279 |
|
| 1280 |
-
@media (max-width:
|
| 1281 |
-
.
|
| 1282 |
-
.
|
| 1283 |
.panels { grid-template-columns: 1fr; }
|
| 1284 |
.panel.full { grid-column: span 1; }
|
| 1285 |
-
.form-row, .form-row-
|
| 1286 |
-
.metrics-grid { flex-wrap: wrap; }
|
| 1287 |
}
|
| 1288 |
</style>
|
| 1289 |
</head>
|
| 1290 |
<body>
|
| 1291 |
-
<div class="
|
| 1292 |
-
<div class="
|
| 1293 |
-
<div class="header
|
| 1294 |
-
<
|
| 1295 |
-
|
| 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
|
| 1304 |
-
{{
|
| 1305 |
</div>
|
| 1306 |
</div>
|
| 1307 |
|
| 1308 |
-
<div class="
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 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="
|
| 1323 |
-
|
| 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="
|
| 1332 |
-
<div class="panel
|
| 1333 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1349 |
-
|
| 1350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1351 |
<div class="form-group">
|
| 1352 |
-
<label>
|
| 1353 |
-
<input type="text" name="
|
| 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="
|
| 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="
|
| 1372 |
</div>
|
| 1373 |
<div class="form-group">
|
| 1374 |
-
<label>Context
|
| 1375 |
-
<input type="text" name="context_path" value=".
|
| 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=
|
| 1386 |
-
<div class="form-hint">Comma-separated key=value pairs</div>
|
| 1387 |
</div>
|
| 1388 |
<div class="form-group">
|
| 1389 |
-
<label>GitHub Token
|
| 1390 |
<input type="password" name="github_token" placeholder="ghp_...">
|
| 1391 |
</div>
|
|
|
|
|
|
|
|
|
|
| 1392 |
</div>
|
| 1393 |
-
</
|
| 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>
|