forgeo-cli 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
forgeo/web/style.css ADDED
@@ -0,0 +1,656 @@
1
+ :root {
2
+ --bg: #0e1015;
3
+ --surface: #151922;
4
+ --surface-hover: #1a1f2a;
5
+ --border: #262c38;
6
+ --border-strong: #333b4a;
7
+ --text: #e7eaf1;
8
+ --text-muted: #9aa4b5;
9
+ --text-faint: #6b7485;
10
+ --accent: #7c8cff;
11
+ --accent-dim: rgba(124, 140, 255, 0.14);
12
+
13
+ --open: #8fb7ff;
14
+ --blocked: #f0a84d;
15
+ --completed: #6fce93;
16
+ --failed: #e06c75;
17
+
18
+ --radius: 12px;
19
+ --radius-sm: 8px;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ html,
27
+ body {
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+
32
+ body {
33
+ min-height: 100vh;
34
+ background:
35
+ radial-gradient(1200px 500px at 50% -10%, rgba(124, 140, 255, 0.07), transparent 60%),
36
+ var(--bg);
37
+ color: var(--text);
38
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
39
+ Arial, sans-serif;
40
+ font-size: 15px;
41
+ line-height: 1.5;
42
+ -webkit-font-smoothing: antialiased;
43
+ text-rendering: optimizeLegibility;
44
+ }
45
+
46
+ .shell {
47
+ max-width: 1200px;
48
+ margin: 0 auto;
49
+ padding: 0 28px 28px;
50
+ display: flex;
51
+ flex-direction: column;
52
+ min-height: 100vh;
53
+ }
54
+
55
+ /* ---------- Header ---------- */
56
+
57
+ .topbar {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ gap: 24px;
62
+ flex-wrap: wrap;
63
+ padding: 22px 0 18px;
64
+ border-bottom: 1px solid var(--border);
65
+ }
66
+
67
+ .brand {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 12px;
71
+ }
72
+
73
+ .brand-dot {
74
+ width: 10px;
75
+ height: 10px;
76
+ border-radius: 50%;
77
+ background: var(--accent);
78
+ box-shadow: 0 0 0 4px var(--accent-dim);
79
+ }
80
+
81
+ .brand-name {
82
+ font-size: 18px;
83
+ font-weight: 600;
84
+ letter-spacing: 0.01em;
85
+ }
86
+
87
+ .meta {
88
+ display: flex;
89
+ gap: 6px 30px;
90
+ flex-wrap: wrap;
91
+ margin: 0;
92
+ }
93
+
94
+ .meta-item {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 2px;
98
+ }
99
+
100
+ .meta-item dt {
101
+ font-size: 11px;
102
+ text-transform: uppercase;
103
+ letter-spacing: 0.09em;
104
+ color: var(--text-faint);
105
+ }
106
+
107
+ .meta-item dd {
108
+ margin: 0;
109
+ font-size: 13px;
110
+ font-variant-numeric: tabular-nums;
111
+ color: var(--text-muted);
112
+ white-space: nowrap;
113
+ }
114
+
115
+ /* ---------- New task form ---------- */
116
+
117
+ .new-task {
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: flex-start;
121
+ gap: 14px;
122
+ padding: 16px 0;
123
+ border-bottom: 1px solid var(--border);
124
+ }
125
+
126
+ .new-task__fields {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 14px;
130
+ width: 100%;
131
+ min-width: 0;
132
+ }
133
+
134
+ .new-task__field {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 6px;
138
+ font-size: 13px;
139
+ color: var(--text-muted);
140
+ }
141
+
142
+ .new-task input[type="text"],
143
+ .new-task textarea {
144
+ width: 100%;
145
+ max-width: 640px;
146
+ min-width: 0;
147
+ background: var(--surface);
148
+ border: 1px solid var(--border);
149
+ border-radius: var(--radius-sm);
150
+ color: var(--text);
151
+ font: inherit;
152
+ font-size: 14px;
153
+ padding: 9px 12px;
154
+ outline: none;
155
+ transition: border-color 0.18s ease, background 0.18s ease;
156
+ }
157
+
158
+ .new-task textarea {
159
+ resize: vertical;
160
+ }
161
+
162
+ .new-task input[type="text"]::placeholder,
163
+ .new-task textarea::placeholder {
164
+ color: var(--text-faint);
165
+ }
166
+
167
+ .new-task input[type="text"]:focus,
168
+ .new-task textarea:focus {
169
+ border-color: var(--accent);
170
+ background: var(--surface-hover);
171
+ }
172
+
173
+ .new-task__criteria-row {
174
+ display: flex;
175
+ gap: 8px;
176
+ width: 100%;
177
+ max-width: 640px;
178
+ }
179
+
180
+ .new-task__criteria-row input[type="text"] {
181
+ flex: 1;
182
+ width: auto;
183
+ max-width: none;
184
+ }
185
+
186
+ .new-task__add {
187
+ flex: none;
188
+ width: 38px;
189
+ padding: 9px 0;
190
+ font-size: 18px;
191
+ line-height: 1;
192
+ }
193
+
194
+ .new-task button {
195
+ background: var(--accent-dim);
196
+ border: 1px solid rgba(124, 140, 255, 0.45);
197
+ border-radius: var(--radius-sm);
198
+ color: var(--accent);
199
+ font: inherit;
200
+ font-size: 14px;
201
+ font-weight: 600;
202
+ padding: 9px 18px;
203
+ cursor: pointer;
204
+ transition: background 0.18s ease, border-color 0.18s ease;
205
+ }
206
+
207
+ .new-task button:hover {
208
+ background: rgba(124, 140, 255, 0.24);
209
+ border-color: var(--accent);
210
+ }
211
+
212
+ .new-task__criteria-list {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: 6px;
216
+ margin: 0;
217
+ padding: 0;
218
+ list-style: none;
219
+ }
220
+
221
+ .new-task__criteria-chip {
222
+ display: inline-flex;
223
+ align-items: center;
224
+ gap: 6px;
225
+ background: var(--surface);
226
+ border: 1px solid var(--border);
227
+ border-radius: 999px;
228
+ color: var(--text);
229
+ font-size: 13px;
230
+ padding: 4px 10px;
231
+ }
232
+
233
+ .new-task__criteria-remove {
234
+ border: 0;
235
+ background: none;
236
+ color: var(--text-faint);
237
+ font-size: 14px;
238
+ line-height: 1;
239
+ padding: 0 2px;
240
+ cursor: pointer;
241
+ }
242
+
243
+ .new-task__criteria-remove:hover {
244
+ color: var(--failed);
245
+ }
246
+
247
+ .new-task__error {
248
+ margin: 0;
249
+ font-size: 13px;
250
+ color: var(--failed);
251
+ }
252
+
253
+ .new-task__error[hidden] {
254
+ display: none;
255
+ }
256
+
257
+ /* ---------- Board ---------- */
258
+
259
+ .board {
260
+ flex: 1;
261
+ padding: 32px 0 40px;
262
+ display: grid;
263
+ grid-template-columns: repeat(4, minmax(0, 1fr));
264
+ gap: 18px;
265
+ align-items: start;
266
+ }
267
+
268
+ .status-col {
269
+ background: var(--surface);
270
+ border: 1px solid var(--border);
271
+ border-radius: var(--radius);
272
+ padding: 14px;
273
+ min-height: 120px;
274
+ transition: border-color 0.18s ease, background 0.18s ease;
275
+ }
276
+
277
+ .status-col:hover {
278
+ border-color: var(--border-strong);
279
+ }
280
+
281
+ .status-col__head {
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: space-between;
285
+ padding: 4px 4px 12px;
286
+ border-bottom: 1px solid var(--border);
287
+ margin-bottom: 12px;
288
+ }
289
+
290
+ .status-col__label {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 8px;
294
+ font-size: 12px;
295
+ font-weight: 600;
296
+ letter-spacing: 0.1em;
297
+ text-transform: uppercase;
298
+ }
299
+
300
+ .status-col__dot {
301
+ width: 8px;
302
+ height: 8px;
303
+ border-radius: 50%;
304
+ }
305
+
306
+ .status-col__count {
307
+ font-size: 12px;
308
+ color: var(--text-faint);
309
+ font-variant-numeric: tabular-nums;
310
+ padding: 1px 8px;
311
+ border-radius: 999px;
312
+ border: 1px solid var(--border);
313
+ }
314
+
315
+ .status-col__empty {
316
+ color: var(--text-faint);
317
+ font-size: 13px;
318
+ padding: 8px 4px;
319
+ font-style: italic;
320
+ }
321
+
322
+ .status-col[data-status="OPEN"] .status-col__dot {
323
+ background: var(--open);
324
+ }
325
+ .status-col[data-status="BLOCKED"] .status-col__dot {
326
+ background: var(--blocked);
327
+ box-shadow: 0 0 0 4px rgba(240, 168, 77, 0.15);
328
+ }
329
+ .status-col[data-status="COMPLETED"] .status-col__dot {
330
+ background: var(--completed);
331
+ }
332
+ .status-col[data-status="FAILED"] .status-col__dot {
333
+ background: var(--failed);
334
+ }
335
+
336
+ /* ---------- Cards ---------- */
337
+
338
+ .task {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 8px;
342
+ background: var(--surface);
343
+ border: 1px solid var(--border);
344
+ border-radius: var(--radius-sm);
345
+ padding: 14px 14px 12px;
346
+ margin-bottom: 10px;
347
+ cursor: pointer;
348
+ transition: transform 0.15s ease, border-color 0.18s ease, background 0.18s ease;
349
+ }
350
+
351
+ .task:hover {
352
+ transform: translateY(-1px);
353
+ background: var(--surface-hover);
354
+ border-color: var(--border-strong);
355
+ }
356
+
357
+ .task__top {
358
+ display: flex;
359
+ align-items: baseline;
360
+ justify-content: space-between;
361
+ gap: 10px;
362
+ }
363
+
364
+ .task__id {
365
+ font-size: 11px;
366
+ font-weight: 600;
367
+ letter-spacing: 0.06em;
368
+ color: var(--text-faint);
369
+ font-variant-numeric: tabular-nums;
370
+ }
371
+
372
+ .badge {
373
+ font-size: 10px;
374
+ font-weight: 600;
375
+ letter-spacing: 0.08em;
376
+ text-transform: uppercase;
377
+ padding: 3px 8px;
378
+ border-radius: 999px;
379
+ white-space: nowrap;
380
+ }
381
+
382
+ .badge--OPEN {
383
+ color: var(--open);
384
+ background: rgba(143, 183, 255, 0.1);
385
+ }
386
+ .badge--BLOCKED {
387
+ color: var(--blocked);
388
+ background: rgba(240, 168, 77, 0.16);
389
+ border: 1px solid rgba(240, 168, 77, 0.45);
390
+ }
391
+ .badge--COMPLETED {
392
+ color: var(--completed);
393
+ background: rgba(111, 206, 147, 0.1);
394
+ }
395
+ .badge--FAILED {
396
+ color: var(--failed);
397
+ background: rgba(224, 108, 117, 0.1);
398
+ }
399
+
400
+ .task__title {
401
+ margin: 0;
402
+ font-size: 14px;
403
+ font-weight: 600;
404
+ line-height: 1.4;
405
+ }
406
+
407
+ .task__desc {
408
+ margin: 0;
409
+ font-size: 13px;
410
+ color: var(--text-muted);
411
+ display: -webkit-box;
412
+ -webkit-line-clamp: 3;
413
+ -webkit-box-orient: vertical;
414
+ overflow: hidden;
415
+ word-break: break-word;
416
+ }
417
+
418
+ .task__times {
419
+ display: flex;
420
+ gap: 14px;
421
+ margin-top: 2px;
422
+ font-size: 11px;
423
+ color: var(--text-faint);
424
+ font-variant-numeric: tabular-nums;
425
+ }
426
+
427
+ .task__times time {
428
+ white-space: nowrap;
429
+ }
430
+
431
+ /* ---------- Task detail modal ---------- */
432
+
433
+ .modal-backdrop {
434
+ position: fixed;
435
+ inset: 0;
436
+ z-index: 50;
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ padding: 24px;
441
+ background: rgba(10, 12, 16, 0.72);
442
+ backdrop-filter: blur(3px);
443
+ }
444
+
445
+ .modal-backdrop[hidden] {
446
+ display: none;
447
+ }
448
+
449
+ .modal {
450
+ width: 100%;
451
+ max-width: 640px;
452
+ max-height: min(80vh, 720px);
453
+ overflow-y: auto;
454
+ background: var(--surface);
455
+ border: 1px solid var(--border-strong);
456
+ border-radius: var(--radius);
457
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
458
+ outline: none;
459
+ }
460
+
461
+ .modal__head {
462
+ display: flex;
463
+ align-items: flex-start;
464
+ justify-content: space-between;
465
+ gap: 16px;
466
+ padding: 18px 20px 14px;
467
+ border-bottom: 1px solid var(--border);
468
+ }
469
+
470
+ .modal__headings {
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 4px;
474
+ min-width: 0;
475
+ }
476
+
477
+ .modal__id {
478
+ font-size: 11px;
479
+ font-weight: 600;
480
+ letter-spacing: 0.06em;
481
+ color: var(--text-faint);
482
+ font-variant-numeric: tabular-nums;
483
+ }
484
+
485
+ .modal__title {
486
+ margin: 0;
487
+ font-size: 18px;
488
+ font-weight: 600;
489
+ line-height: 1.35;
490
+ word-break: break-word;
491
+ }
492
+
493
+ .modal__close {
494
+ flex: none;
495
+ appearance: none;
496
+ background: var(--surface-hover);
497
+ border: 1px solid var(--border);
498
+ border-radius: var(--radius-sm);
499
+ color: var(--text-muted);
500
+ font: inherit;
501
+ font-size: 16px;
502
+ line-height: 1;
503
+ width: 30px;
504
+ height: 30px;
505
+ cursor: pointer;
506
+ transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
507
+ }
508
+
509
+ .modal__close:hover {
510
+ color: var(--text);
511
+ border-color: var(--border-strong);
512
+ background: var(--border);
513
+ }
514
+
515
+ .modal__body {
516
+ padding: 16px 20px 20px;
517
+ display: flex;
518
+ flex-direction: column;
519
+ gap: 16px;
520
+ }
521
+
522
+ .modal__section h3 {
523
+ margin: 0 0 6px;
524
+ font-size: 11px;
525
+ font-weight: 600;
526
+ text-transform: uppercase;
527
+ letter-spacing: 0.09em;
528
+ color: var(--text-faint);
529
+ }
530
+
531
+ .modal__desc {
532
+ margin: 0;
533
+ font-size: 14px;
534
+ color: var(--text);
535
+ white-space: pre-wrap;
536
+ word-break: break-word;
537
+ }
538
+
539
+ .modal__list {
540
+ margin: 0;
541
+ padding-left: 18px;
542
+ font-size: 13px;
543
+ color: var(--text-muted);
544
+ }
545
+
546
+ .modal__list li + li {
547
+ margin-top: 4px;
548
+ }
549
+
550
+ .modal__empty {
551
+ list-style: none;
552
+ margin-left: -18px;
553
+ color: var(--text-faint);
554
+ }
555
+
556
+ .modal__command {
557
+ margin: 0;
558
+ background: var(--bg);
559
+ border: 1px solid var(--border);
560
+ border-radius: var(--radius-sm);
561
+ padding: 10px 12px;
562
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
563
+ font-size: 12.5px;
564
+ color: var(--text-muted);
565
+ white-space: pre-wrap;
566
+ word-break: break-word;
567
+ }
568
+
569
+ .modal__times {
570
+ display: flex;
571
+ gap: 18px;
572
+ padding-top: 4px;
573
+ border-top: 1px solid var(--border);
574
+ font-size: 12px;
575
+ color: var(--text-faint);
576
+ font-variant-numeric: tabular-nums;
577
+ }
578
+
579
+ .modal__times time {
580
+ white-space: nowrap;
581
+ }
582
+
583
+ /* ---------- Empty state ---------- */
584
+
585
+ .empty-state {
586
+ grid-column: 1 / -1;
587
+ text-align: center;
588
+ padding: 90px 24px;
589
+ border: 1px dashed var(--border-strong);
590
+ border-radius: var(--radius);
591
+ }
592
+
593
+ .empty-title {
594
+ margin: 0 0 6px;
595
+ font-size: 16px;
596
+ font-weight: 600;
597
+ }
598
+
599
+ .empty-sub {
600
+ margin: 0;
601
+ color: var(--text-muted);
602
+ font-size: 13px;
603
+ }
604
+
605
+ /* ---------- Footer ---------- */
606
+
607
+ .footerbar {
608
+ display: flex;
609
+ align-items: center;
610
+ justify-content: space-between;
611
+ gap: 16px;
612
+ padding-top: 16px;
613
+ border-top: 1px solid var(--border);
614
+ font-size: 12px;
615
+ color: var(--text-faint);
616
+ }
617
+
618
+ .notice {
619
+ color: var(--blocked);
620
+ background: rgba(240, 168, 77, 0.08);
621
+ border: 1px solid rgba(240, 168, 77, 0.25);
622
+ padding: 4px 10px;
623
+ border-radius: 999px;
624
+ }
625
+
626
+ .notice[hidden] {
627
+ display: none;
628
+ }
629
+
630
+ .fetch-time {
631
+ font-variant-numeric: tabular-nums;
632
+ }
633
+
634
+ .fetch-time[data-stale="true"] {
635
+ color: var(--blocked);
636
+ }
637
+
638
+ /* ---------- Responsive ---------- */
639
+
640
+ @media (max-width: 960px) {
641
+ .board {
642
+ grid-template-columns: repeat(2, minmax(0, 1fr));
643
+ }
644
+ }
645
+
646
+ @media (max-width: 560px) {
647
+ .shell {
648
+ padding: 0 16px 20px;
649
+ }
650
+ .board {
651
+ grid-template-columns: 1fr;
652
+ }
653
+ .meta {
654
+ gap: 6px 22px;
655
+ }
656
+ }
forgeo/web_common.py ADDED
@@ -0,0 +1,92 @@
1
+ """Helpers for the central web dashboard (:mod:`forgeo.central`).
2
+
3
+ The dashboard speaks JSON and serves static files over the stdlib
4
+ ``http.server``; these small, pure helpers keep the handler consistent
5
+ (same bounds, same static path guards, same serialization).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import mimetypes
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.parse import unquote
16
+
17
+ WEB_ROOT = Path(__file__).resolve().parent / "web"
18
+
19
+ DEFAULT_LOG_LINES = 100
20
+ MAX_LOG_LINES = 10_000
21
+ DEFAULT_RUN_LIMIT = 10
22
+ MAX_RUN_LIMIT = 10_000
23
+
24
+
25
+ def json_bytes(payload: Any) -> bytes:
26
+ """Serialize ``payload`` to pretty JSON bytes with a trailing newline."""
27
+ return json.dumps(payload, indent=2, default=str).encode("utf-8") + b"\n"
28
+
29
+
30
+ def iso(value: datetime) -> str:
31
+ """Render a datetime as an ISO-8601 string with an explicit UTC offset."""
32
+ if value.tzinfo is None:
33
+ return value.isoformat() + "Z"
34
+ return value.isoformat()
35
+
36
+
37
+ def tail_lines(path: Path, n: int) -> list[str]:
38
+ """Return the last ``n`` lines of ``path``, tolerating a missing file.
39
+
40
+ A missing or unreadable file reads as an empty list.
41
+ """
42
+ if n <= 0 or not path.exists():
43
+ return []
44
+ try:
45
+ text = path.read_text(encoding="utf-8", errors="replace")
46
+ except OSError:
47
+ return []
48
+ lines = text.splitlines()
49
+ if n >= len(lines):
50
+ return lines
51
+ return lines[-n:]
52
+
53
+
54
+ def clamp_query_int(
55
+ query: dict[str, list[str]], key: str, default: int, maximum: int
56
+ ) -> int:
57
+ """Parse a bounded non-negative integer from a query parameter."""
58
+ raw = query.get(key, [str(default)])[0]
59
+ try:
60
+ value = int(raw)
61
+ except ValueError:
62
+ value = default
63
+ return max(0, min(value, maximum))
64
+
65
+
66
+ def safe_static_path(url_path: str, root: Path = WEB_ROOT) -> Path | None:
67
+ """Resolve a URL path under ``root``, rejecting traversal.
68
+
69
+ ``/`` and trailing slashes resolve to ``index.html``; a resolved path
70
+ that escapes ``root`` or is not a regular file returns ``None``.
71
+ """
72
+ rel = unquote(url_path).lstrip("/")
73
+ if not rel:
74
+ rel = "index.html"
75
+ elif rel.endswith("/"):
76
+ rel = rel + "index.html"
77
+ root = root.resolve()
78
+ if not root.is_dir():
79
+ return None
80
+ candidate = (root / rel).resolve()
81
+ try:
82
+ candidate.relative_to(root)
83
+ except ValueError:
84
+ return None
85
+ if candidate.is_file():
86
+ return candidate
87
+ return None
88
+
89
+
90
+ def guess_content_type(path: Path) -> str:
91
+ """A MIME type for a static file, falling back to ``application/octet-stream``."""
92
+ return mimetypes.guess_type(str(path))[0] or "application/octet-stream"