withcache 0.6.2__tar.gz → 0.6.4__tar.gz
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.
- {withcache-0.6.2 → withcache-0.6.4}/PKG-INFO +15 -1
- {withcache-0.6.2 → withcache-0.6.4}/README.md +14 -0
- {withcache-0.6.2 → withcache-0.6.4}/shim/build.zig.zon +1 -1
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/__init__.py +1 -1
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/server.py +196 -132
- withcache-0.6.4/src/withcache/static/bootstrap-icons.min.css +5 -0
- withcache-0.6.4/src/withcache/static/bootstrap.min.css +12 -0
- withcache-0.6.4/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- withcache-0.6.4/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/tests/test_differential.py +1 -1
- {withcache-0.6.2 → withcache-0.6.4}/tests/test_oras.py +16 -10
- {withcache-0.6.2 → withcache-0.6.4}/tests/test_withcache.py +10 -12
- withcache-0.6.2/src/withcache/static/pico.min.css +0 -4
- {withcache-0.6.2 → withcache-0.6.4}/.gitignore +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/LICENSE +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/deploy/Containerfile +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/deploy/compose.yml +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/hatch_build.py +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/pyproject.toml +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/shim/build.zig +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/shim/shim.zig +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/_shim.py +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/client.py +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/oras.py +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.6.2 → withcache-0.6.4}/src/withcache/wgetwithcache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/withcache
|
|
6
6
|
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
@@ -285,6 +285,20 @@ the probe records the miss and, in auto-fetch mode, enqueues the fill, so the
|
|
|
285
285
|
next call flips to the cache. The encoding is shared with the shims and server,
|
|
286
286
|
so consumers stay in lockstep with the cache-host.
|
|
287
287
|
|
|
288
|
+
### Pull an `oras://` artifact (oras + client together)
|
|
289
|
+
|
|
290
|
+
For a registry blob, pair `withcache.oras` (resolve the reference to a blob URL
|
|
291
|
+
+ bearer) with `client.serve_url` (prefer the cache when warm). A runnable
|
|
292
|
+
example is in [`examples/pull_oras_blob.py`](examples/pull_oras_blob.py):
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from withcache import client, oras
|
|
296
|
+
|
|
297
|
+
resolved = oras.resolve_ref("oras://ghcr.io/<owner>/<repo>@sha256:<digest>")
|
|
298
|
+
url, headers = resolved.blob_url, dict(resolved.headers)
|
|
299
|
+
url = client.serve_url(server, url, headers=headers) or url # cache when warm
|
|
300
|
+
```
|
|
301
|
+
|
|
288
302
|
## Tests
|
|
289
303
|
|
|
290
304
|
```sh
|
|
@@ -267,6 +267,20 @@ the probe records the miss and, in auto-fetch mode, enqueues the fill, so the
|
|
|
267
267
|
next call flips to the cache. The encoding is shared with the shims and server,
|
|
268
268
|
so consumers stay in lockstep with the cache-host.
|
|
269
269
|
|
|
270
|
+
### Pull an `oras://` artifact (oras + client together)
|
|
271
|
+
|
|
272
|
+
For a registry blob, pair `withcache.oras` (resolve the reference to a blob URL
|
|
273
|
+
+ bearer) with `client.serve_url` (prefer the cache when warm). A runnable
|
|
274
|
+
example is in [`examples/pull_oras_blob.py`](examples/pull_oras_blob.py):
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from withcache import client, oras
|
|
278
|
+
|
|
279
|
+
resolved = oras.resolve_ref("oras://ghcr.io/<owner>/<repo>@sha256:<digest>")
|
|
280
|
+
url, headers = resolved.blob_url, dict(resolved.headers)
|
|
281
|
+
url = client.serve_url(server, url, headers=headers) or url # cache when warm
|
|
282
|
+
```
|
|
283
|
+
|
|
270
284
|
## Tests
|
|
271
285
|
|
|
272
286
|
```sh
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.name = .withcache_shim,
|
|
3
3
|
// Zig requires a literal here; keep it in lockstep with the project's
|
|
4
4
|
// single source (src/withcache/__init__.py) via `make bump` / `make version-check`.
|
|
5
|
-
.version = "0.6.
|
|
5
|
+
.version = "0.6.4",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -17,6 +17,6 @@ All modules are stdlib-only and self-contained.
|
|
|
17
17
|
from . import oras
|
|
18
18
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
19
19
|
|
|
20
|
-
__version__ = "0.6.
|
|
20
|
+
__version__ = "0.6.4"
|
|
21
21
|
|
|
22
22
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -22,6 +22,7 @@ admin password is set, the operator UI is left open (with a startup warning).
|
|
|
22
22
|
|
|
23
23
|
import argparse
|
|
24
24
|
import base64
|
|
25
|
+
import contextlib
|
|
25
26
|
import hashlib
|
|
26
27
|
import hmac
|
|
27
28
|
import html
|
|
@@ -40,6 +41,7 @@ import urllib.parse
|
|
|
40
41
|
import urllib.request
|
|
41
42
|
from dataclasses import dataclass, field
|
|
42
43
|
from datetime import datetime, timezone
|
|
44
|
+
from typing import ClassVar
|
|
43
45
|
|
|
44
46
|
from . import __version__, oras
|
|
45
47
|
|
|
@@ -56,7 +58,14 @@ USER_AGENT = f"withcache-cache/{__version__}"
|
|
|
56
58
|
# operating depth.
|
|
57
59
|
RESUME_MAX_ATTEMPTS = 5
|
|
58
60
|
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|
59
|
-
MIME_TYPES = {
|
|
61
|
+
MIME_TYPES = {
|
|
62
|
+
".css": "text/css; charset=utf-8",
|
|
63
|
+
".js": "application/javascript; charset=utf-8",
|
|
64
|
+
".woff": "font/woff",
|
|
65
|
+
".woff2": "font/woff2",
|
|
66
|
+
".png": "image/png",
|
|
67
|
+
".svg": "image/svg+xml",
|
|
68
|
+
}
|
|
60
69
|
_DB_WRITE_LOCK = threading.Lock()
|
|
61
70
|
|
|
62
71
|
|
|
@@ -304,10 +313,8 @@ class Store:
|
|
|
304
313
|
"""Drop a cached artifact (row + bytes). The manual half of eviction."""
|
|
305
314
|
with _DB_WRITE_LOCK, self.conn() as c:
|
|
306
315
|
c.execute("DELETE FROM blobs WHERE key=?", (key,))
|
|
307
|
-
|
|
316
|
+
with contextlib.suppress(FileNotFoundError):
|
|
308
317
|
os.remove(self.blob_path(key))
|
|
309
|
-
except FileNotFoundError:
|
|
310
|
-
pass
|
|
311
318
|
|
|
312
319
|
def store_from_origin(
|
|
313
320
|
self,
|
|
@@ -471,7 +478,6 @@ class Store:
|
|
|
471
478
|
# --------------------------------------------------------------------------
|
|
472
479
|
# Background download manager (thread pool; modelled on bty's job managers)
|
|
473
480
|
# --------------------------------------------------------------------------
|
|
474
|
-
JOB_STATES = ("queued", "running", "completed", "cancelled", "failed")
|
|
475
481
|
PENDING_STATES = frozenset(("queued", "running"))
|
|
476
482
|
|
|
477
483
|
|
|
@@ -841,14 +847,24 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
841
847
|
return self.headers.get("HX-Request") == "true"
|
|
842
848
|
|
|
843
849
|
def serve_static(self, parsed):
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
850
|
+
"""Serve files under ``src/withcache/static/`` (Bootstrap CSS +
|
|
851
|
+
Bootstrap Icons CSS + htmx.min.js + the icon font files under
|
|
852
|
+
``static/fonts/`` that bootstrap-icons.min.css references via
|
|
853
|
+
a relative ``fonts/…`` src). Constrain to ``static/`` and
|
|
854
|
+
``static/fonts/`` explicitly; abspath+startswith rejects any
|
|
855
|
+
``..`` traversal past the static root."""
|
|
856
|
+
rel = parsed.path[len("/static/") :]
|
|
857
|
+
if not rel or rel.endswith("/"):
|
|
847
858
|
self.send_text(404, "not found\n")
|
|
848
859
|
return
|
|
849
|
-
|
|
860
|
+
target = os.path.abspath(os.path.join(STATIC_DIR, rel))
|
|
861
|
+
static_root = os.path.abspath(STATIC_DIR) + os.sep
|
|
862
|
+
if not target.startswith(static_root) or not os.path.isfile(target):
|
|
863
|
+
self.send_text(404, "not found\n")
|
|
864
|
+
return
|
|
865
|
+
with open(target, "rb") as f:
|
|
850
866
|
data = f.read()
|
|
851
|
-
ext = os.path.splitext(
|
|
867
|
+
ext = os.path.splitext(target)[1]
|
|
852
868
|
self.send_response(200)
|
|
853
869
|
self.send_header("Content-Type", MIME_TYPES.get(ext, "application/octet-stream"))
|
|
854
870
|
self.send_header("Content-Length", str(len(data)))
|
|
@@ -1012,89 +1028,144 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1012
1028
|
self.end_headers()
|
|
1013
1029
|
|
|
1014
1030
|
# -- HTML --------------------------------------------------------------
|
|
1015
|
-
STATUS_COLORS = {
|
|
1016
|
-
"queued": "
|
|
1017
|
-
"running": "var(--
|
|
1018
|
-
"completed": "
|
|
1019
|
-
"failed": "
|
|
1020
|
-
"cancelled": "
|
|
1031
|
+
STATUS_COLORS: ClassVar[dict[str, str]] = {
|
|
1032
|
+
"queued": "var(--bs-secondary)",
|
|
1033
|
+
"running": "var(--bs-primary)",
|
|
1034
|
+
"completed": "var(--bs-success)",
|
|
1035
|
+
"failed": "var(--bs-danger)",
|
|
1036
|
+
"cancelled": "var(--bs-secondary)",
|
|
1021
1037
|
}
|
|
1022
1038
|
|
|
1039
|
+
# bty ships a Bootstrap 5 stack (bootstrap.min.css +
|
|
1040
|
+
# bootstrap-icons.min.css + htmx). All three ecosystem services
|
|
1041
|
+
# (bty, nbdmux, withcache) share that stack so operators only
|
|
1042
|
+
# learn one UI grammar; the primary hue is what tells them
|
|
1043
|
+
# which service they're on. The trio sits on a
|
|
1044
|
+
# navy -> dark-magenta -> magenta gradient (cool -> hot);
|
|
1045
|
+
# withcache is the dark-magenta middle (the byte cache that
|
|
1046
|
+
# feeds nbdmux and every other consumer).
|
|
1047
|
+
_PRIMARY_HEX = "#8f1b71" # dark-magenta
|
|
1048
|
+
_PRIMARY_HOVER = "#7a1861"
|
|
1049
|
+
_PRIMARY_RGB = "143, 27, 113"
|
|
1050
|
+
|
|
1023
1051
|
def _head(self, title: str) -> str:
|
|
1052
|
+
primary = self._PRIMARY_HEX
|
|
1053
|
+
hover = self._PRIMARY_HOVER
|
|
1054
|
+
rgb = self._PRIMARY_RGB
|
|
1024
1055
|
return f"""<!doctype html>
|
|
1025
1056
|
<html lang="en"><head>
|
|
1026
1057
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1027
1058
|
<title>{title}</title>
|
|
1028
|
-
<link rel="stylesheet" href="/static/
|
|
1059
|
+
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
|
1060
|
+
<link rel="stylesheet" href="/static/bootstrap-icons.min.css">
|
|
1029
1061
|
<script src="/static/htmx.min.js"></script>
|
|
1030
1062
|
<style>
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1063
|
+
/* Bootstrap 5 exposes --bs-primary + a matching -rgb triplet
|
|
1064
|
+
used for translucent variants (alerts, focus rings, .bg-*-
|
|
1065
|
+
subtle). Overriding both here re-tints every stock component
|
|
1066
|
+
without patching bootstrap.min.css. */
|
|
1067
|
+
:root {{
|
|
1068
|
+
--bs-primary: {primary};
|
|
1069
|
+
--bs-primary-rgb: {rgb};
|
|
1070
|
+
--bs-link-color: {primary};
|
|
1071
|
+
--bs-link-hover-color: {hover};
|
|
1072
|
+
}}
|
|
1073
|
+
.btn-primary {{ --bs-btn-bg: {primary}; --bs-btn-border-color: {primary};
|
|
1074
|
+
--bs-btn-hover-bg: {hover}; --bs-btn-hover-border-color: {hover};
|
|
1075
|
+
--bs-btn-active-bg: {hover}; --bs-btn-active-border-color: {hover}; }}
|
|
1076
|
+
.bg-primary {{ --bs-bg-opacity: 1; background-color: {primary} !important; }}
|
|
1077
|
+
.text-primary {{ --bs-text-opacity: 1; color: {primary} !important; }}
|
|
1078
|
+
.border-primary {{ --bs-border-opacity: 1; border-color: {primary} !important; }}
|
|
1079
|
+
/* Brand strip: navy -> dark-magenta -> magenta gradient shared
|
|
1080
|
+
across bty (navy), withcache (dark-magenta) and nbdmux
|
|
1081
|
+
(magenta) so the trio reads as one product family from any
|
|
1082
|
+
of the three consoles. */
|
|
1083
|
+
.brand-accent {{ height: 3px;
|
|
1084
|
+
background: linear-gradient(90deg, #0d3585 0%, #8f1b71 50%, #d63384 100%); }}
|
|
1085
|
+
code {{ color: inherit; font-size: .85em; }}
|
|
1034
1086
|
.url {{ word-break: break-all; }}
|
|
1035
1087
|
.num {{ text-align: right; }}
|
|
1036
|
-
.mono {{ font-family: var(--
|
|
1037
|
-
td form {{ display: inline; margin: 0; }}
|
|
1038
|
-
td button {{ width: auto; display: inline-block; margin: 0 .3rem 0 0;
|
|
1039
|
-
padding: .15rem .6rem; font-size: .8rem; }}
|
|
1040
|
-
td progress {{ margin: 0 0 .15rem; }}
|
|
1088
|
+
.mono {{ font-family: var(--bs-font-monospace); font-size: .85em; }}
|
|
1041
1089
|
#spin {{ width: 7rem; height: .5rem; margin: 0; }}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1090
|
+
/* Tabs: Bootstrap nav-tabs, tinted via the active class. The
|
|
1091
|
+
content panels toggle visibility via ``section.tab.active-tab``
|
|
1092
|
+
so the same 1 Hz htmx innerHTML swap that used to work with
|
|
1093
|
+
the Pico shell keeps working here without changing the JS. */
|
|
1094
|
+
nav.tabs .nav-link {{ color: var(--bs-secondary-color); border: none;
|
|
1095
|
+
border-bottom: 2px solid transparent; padding: .45rem .9rem;
|
|
1096
|
+
font-size: .9rem; margin-bottom: -1px; }}
|
|
1097
|
+
nav.tabs .nav-link:hover {{ color: var(--bs-body-color); }}
|
|
1098
|
+
nav.tabs .nav-link.active-tab {{ color: var(--bs-body-color);
|
|
1099
|
+
border-bottom-color: var(--bs-primary); font-weight: 600; }}
|
|
1100
|
+
section.tab {{ display: none; padding-top: .75rem; }}
|
|
1101
|
+
section.tab.active-tab {{ display: block; }}
|
|
1045
1102
|
</style>
|
|
1046
1103
|
</head>"""
|
|
1047
1104
|
|
|
1048
1105
|
def render_login(self, error: str = "") -> str:
|
|
1049
|
-
err = f'<div class="
|
|
1050
|
-
return f"""{self._head("withcache
|
|
1051
|
-
<body
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
<
|
|
1061
|
-
|
|
1062
|
-
|
|
1106
|
+
err = f'<div class="alert alert-danger">{html.escape(error)}</div>' if error else ""
|
|
1107
|
+
return f"""{self._head("withcache - login")}
|
|
1108
|
+
<body>
|
|
1109
|
+
<div class="brand-accent"></div>
|
|
1110
|
+
<main class="container py-5">
|
|
1111
|
+
<div class="card mx-auto" style="max-width: 24rem;">
|
|
1112
|
+
<div class="card-body">
|
|
1113
|
+
<h3 class="card-title fw-bold text-primary">
|
|
1114
|
+
<i class="bi bi-hdd-stack"></i> withcache
|
|
1115
|
+
<small class="text-muted fs-6">v{html.escape(__version__)}</small>
|
|
1116
|
+
</h3>
|
|
1117
|
+
<p class="text-muted small mb-3">Operator login</p>
|
|
1118
|
+
{err}
|
|
1119
|
+
<form method="post" action="/ui/login">
|
|
1120
|
+
<div class="mb-3">
|
|
1121
|
+
<label class="form-label" for="pw">Admin password</label>
|
|
1122
|
+
<input class="form-control" id="pw" type="password" name="password" autofocus required>
|
|
1123
|
+
</div>
|
|
1124
|
+
<button class="btn btn-primary w-100" type="submit">Log in</button>
|
|
1125
|
+
</form>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1063
1128
|
</main></body></html>"""
|
|
1064
1129
|
|
|
1065
1130
|
def render_page(self) -> str:
|
|
1066
1131
|
logout = (
|
|
1067
|
-
'<
|
|
1068
|
-
'<button type="submit" class="
|
|
1069
|
-
|
|
1132
|
+
'<form method="post" action="/ui/logout" class="d-inline m-0">'
|
|
1133
|
+
'<button type="submit" class="btn btn-sm btn-outline-secondary">'
|
|
1134
|
+
"Log out</button></form>"
|
|
1070
1135
|
if self.auth.enabled
|
|
1071
1136
|
else ""
|
|
1072
1137
|
)
|
|
1073
1138
|
return f"""{self._head("withcache cache-host")}
|
|
1074
|
-
<body
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1139
|
+
<body>
|
|
1140
|
+
<div class="brand-accent"></div>
|
|
1141
|
+
<nav class="navbar navbar-expand navbar-light bg-light border-bottom">
|
|
1142
|
+
<div class="container">
|
|
1143
|
+
<a class="navbar-brand fw-bold text-primary" href="/">
|
|
1144
|
+
<i class="bi bi-hdd-stack"></i> withcache
|
|
1145
|
+
<span class="badge bg-primary bg-opacity-10 text-primary ms-1"
|
|
1146
|
+
>v{html.escape(__version__)}</span>
|
|
1147
|
+
</a>
|
|
1148
|
+
<div class="d-flex align-items-center gap-3">
|
|
1149
|
+
<progress id="spin" class="htmx-indicator"></progress>
|
|
1082
1150
|
{logout}
|
|
1083
|
-
</
|
|
1084
|
-
</
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
<
|
|
1088
|
-
|
|
1089
|
-
<
|
|
1090
|
-
<
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1151
|
+
</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
</nav>
|
|
1154
|
+
<main class="container py-4">
|
|
1155
|
+
<div class="card mb-4">
|
|
1156
|
+
<div class="card-header"><i class="bi bi-cloud-download text-primary"></i> Add from URI</div>
|
|
1157
|
+
<div class="card-body">
|
|
1158
|
+
<form hx-post="/admin/fetch" hx-target="#dash" hx-swap="innerHTML"
|
|
1159
|
+
hx-indicator="#spin" hx-on::after-request="this.reset()">
|
|
1160
|
+
<div class="input-group">
|
|
1161
|
+
<input class="form-control" type="url" name="url"
|
|
1162
|
+
placeholder="https://origin/path/artifact.tar.gz" required>
|
|
1163
|
+
<button class="btn btn-primary" type="submit" style="white-space: nowrap;"
|
|
1164
|
+
>Fetch & store</button>
|
|
1165
|
+
</div>
|
|
1166
|
+
</form>
|
|
1167
|
+
</div>
|
|
1168
|
+
</div>
|
|
1098
1169
|
|
|
1099
1170
|
<!-- The hx-trigger gates polling on the user NOT having an active
|
|
1100
1171
|
text selection, so highlight-and-copy a URL out of a table cell
|
|
@@ -1190,26 +1261,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1190
1261
|
# newly-inserted node is not the one ``:target`` resolved to
|
|
1191
1262
|
# at hash-change time). The visible symptom was the tab
|
|
1192
1263
|
# snapping back to Streams within a second of every click.
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
nav.tabs { margin: 1rem 0 .25rem; border-bottom: 1px solid var(--pico-muted-border-color); }
|
|
1196
|
-
nav.tabs ul { display: flex; gap: 0; padding: 0; margin: 0; list-style: none; }
|
|
1197
|
-
nav.tabs li { margin: 0; }
|
|
1198
|
-
nav.tabs a {
|
|
1199
|
-
display: inline-block; padding: .45rem .9rem; text-decoration: none;
|
|
1200
|
-
color: var(--pico-muted-color); border-bottom: 2px solid transparent;
|
|
1201
|
-
margin-bottom: -1px; font-size: .9rem;
|
|
1202
|
-
}
|
|
1203
|
-
nav.tabs a:hover { color: var(--pico-color); }
|
|
1204
|
-
nav.tabs a.active-tab {
|
|
1205
|
-
color: var(--pico-color);
|
|
1206
|
-
border-bottom-color: var(--pico-primary, #0172ad);
|
|
1207
|
-
font-weight: 600;
|
|
1208
|
-
}
|
|
1209
|
-
section.tab { display: none; padding-top: .75rem; }
|
|
1210
|
-
section.tab.active-tab { display: block; }
|
|
1211
|
-
</style>
|
|
1212
|
-
"""
|
|
1264
|
+
# The class hooks live in _head's <style> so this dash render
|
|
1265
|
+
# can be swapped 1 Hz without re-declaring them.
|
|
1213
1266
|
|
|
1214
1267
|
stream_rows = (
|
|
1215
1268
|
"".join(
|
|
@@ -1221,12 +1274,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1221
1274
|
</tr>"""
|
|
1222
1275
|
for s in streams
|
|
1223
1276
|
)
|
|
1224
|
-
or '<tr><td colspan="4"
|
|
1277
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1278
|
+
"<em>No active streams.</em></td></tr>"
|
|
1225
1279
|
)
|
|
1226
1280
|
|
|
1227
1281
|
job_rows = (
|
|
1228
1282
|
"".join(self._job_row(j) for j in jobs)
|
|
1229
|
-
or '<tr><td colspan="4"
|
|
1283
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1284
|
+
"<em>No downloads yet.</em></td></tr>"
|
|
1230
1285
|
)
|
|
1231
1286
|
|
|
1232
1287
|
miss_rows = (
|
|
@@ -1235,21 +1290,23 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1235
1290
|
<td class="url">{html.escape(m["url"])}</td>
|
|
1236
1291
|
<td class="num">{m["count"]}</td>
|
|
1237
1292
|
<td><small>{html.escape(m["last_seen"])}</small></td>
|
|
1238
|
-
<td>
|
|
1293
|
+
<td class="d-flex gap-2 flex-wrap">
|
|
1239
1294
|
<form hx-post="/admin/fetch" hx-target="#dash"
|
|
1240
|
-
hx-swap="innerHTML" hx-indicator="#spin">
|
|
1295
|
+
hx-swap="innerHTML" hx-indicator="#spin" class="m-0">
|
|
1241
1296
|
<input type="hidden" name="url" value="{html.escape(m["url"], quote=True)}">
|
|
1242
|
-
<button type="submit">Download</button>
|
|
1297
|
+
<button class="btn btn-sm btn-primary" type="submit">Download</button>
|
|
1243
1298
|
</form>
|
|
1244
|
-
<form hx-post="/admin/dismiss" hx-target="#dash" hx-swap="innerHTML">
|
|
1299
|
+
<form hx-post="/admin/dismiss" hx-target="#dash" hx-swap="innerHTML" class="m-0">
|
|
1245
1300
|
<input type="hidden" name="key" value="{html.escape(m["key"], quote=True)}">
|
|
1246
|
-
<button
|
|
1301
|
+
<button class="btn btn-sm btn-outline-secondary" type="submit"
|
|
1302
|
+
>Dismiss</button>
|
|
1247
1303
|
</form>
|
|
1248
1304
|
</td>
|
|
1249
1305
|
</tr>"""
|
|
1250
1306
|
for m in misses
|
|
1251
1307
|
)
|
|
1252
|
-
or '<tr><td colspan="4"
|
|
1308
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1309
|
+
"<em>No misses recorded.</em></td></tr>"
|
|
1253
1310
|
)
|
|
1254
1311
|
|
|
1255
1312
|
# Build the per-row /b/ serve URL once; the cell wraps the
|
|
@@ -1275,19 +1332,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1275
1332
|
<td>{human_size(b["size"])}</td>
|
|
1276
1333
|
<td class="num">{b["hits"]}</td>
|
|
1277
1334
|
<td class="num">{b["misses"]}</td>
|
|
1278
|
-
<td class="mono">{html.escape(b["sha256"][:12])}
|
|
1335
|
+
<td class="mono">{html.escape(b["sha256"][:12])}...</td>
|
|
1279
1336
|
<td><small>{html.escape(b["fetched_at"])}</small></td>
|
|
1280
1337
|
<td>
|
|
1281
1338
|
<form hx-post="/admin/delete" hx-target="#dash" hx-swap="innerHTML"
|
|
1282
|
-
hx-confirm="Delete this cached artifact?">
|
|
1339
|
+
hx-confirm="Delete this cached artifact?" class="m-0">
|
|
1283
1340
|
<input type="hidden" name="key" value="{html.escape(b["key"], quote=True)}">
|
|
1284
|
-
<button
|
|
1341
|
+
<button class="btn btn-sm btn-outline-danger" type="submit"
|
|
1342
|
+
>Delete</button>
|
|
1285
1343
|
</form>
|
|
1286
1344
|
</td>
|
|
1287
1345
|
</tr>"""
|
|
1288
1346
|
for b in blobs
|
|
1289
1347
|
)
|
|
1290
|
-
or '<tr><td colspan="7"
|
|
1348
|
+
or '<tr><td colspan="7" class="text-center text-muted">'
|
|
1349
|
+
"<em>Cache is empty.</em></td></tr>"
|
|
1291
1350
|
)
|
|
1292
1351
|
|
|
1293
1352
|
# Per-tab counts let the operator see at a glance whether each
|
|
@@ -1296,59 +1355,63 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1296
1355
|
njobs = len(jobs)
|
|
1297
1356
|
|
|
1298
1357
|
return f"""
|
|
1299
|
-
<p
|
|
1300
|
-
{
|
|
1301
|
-
<nav class="tabs"
|
|
1302
|
-
<
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1358
|
+
<p class="text-muted small mb-2">{nblobs} cached ({used}){full}
|
|
1359
|
+
· {nmisses} pending miss(es)</p>
|
|
1360
|
+
<nav class="tabs border-bottom mb-2">
|
|
1361
|
+
<ul class="nav">
|
|
1362
|
+
<li class="nav-item"><a class="nav-link {_active("tab-cached").lstrip()}"
|
|
1363
|
+
href="#tab-cached">Cached ({nblobs})</a></li>
|
|
1364
|
+
<li class="nav-item"><a class="nav-link {_active("tab-streams").lstrip()}"
|
|
1365
|
+
href="#tab-streams">Streams ({nstreams})</a></li>
|
|
1366
|
+
<li class="nav-item"><a class="nav-link {_active("tab-downloads").lstrip()}"
|
|
1367
|
+
href="#tab-downloads">Downloads ({njobs})</a></li>
|
|
1368
|
+
<li class="nav-item"><a class="nav-link {_active("tab-misses").lstrip()}"
|
|
1369
|
+
href="#tab-misses">Misses ({nmisses})</a></li>
|
|
1370
|
+
</ul>
|
|
1371
|
+
</nav>
|
|
1311
1372
|
|
|
1312
1373
|
<section id="tab-cached" class="tab{_active("tab-cached")}">
|
|
1313
|
-
<
|
|
1314
|
-
<thead><tr>
|
|
1374
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1375
|
+
<thead class="table-light"><tr>
|
|
1315
1376
|
<th>URL</th><th>Size</th><th class="num">Hits</th><th class="num">Misses</th>
|
|
1316
1377
|
<th>SHA-256</th><th>Fetched</th><th>Action</th>
|
|
1317
1378
|
</tr></thead>
|
|
1318
1379
|
<tbody>{blob_rows}</tbody>
|
|
1319
|
-
</table></
|
|
1380
|
+
</table></div>
|
|
1320
1381
|
</section>
|
|
1321
1382
|
|
|
1322
1383
|
<section id="tab-streams" class="tab{_active("tab-streams")}">
|
|
1323
|
-
<
|
|
1324
|
-
<thead><tr>
|
|
1384
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1385
|
+
<thead class="table-light"><tr>
|
|
1325
1386
|
<th>URL</th><th>Client</th><th>Progress</th><th>Age</th>
|
|
1326
1387
|
</tr></thead>
|
|
1327
1388
|
<tbody>{stream_rows}</tbody>
|
|
1328
|
-
</table></
|
|
1389
|
+
</table></div>
|
|
1329
1390
|
</section>
|
|
1330
1391
|
|
|
1331
1392
|
<section id="tab-downloads" class="tab{_active("tab-downloads")}">
|
|
1332
|
-
<div class="
|
|
1333
|
-
<small>Auto-fetch workers feeding the cache.</small>
|
|
1334
|
-
<form hx-post="/admin/clear" hx-target="#dash" hx-swap="innerHTML"
|
|
1335
|
-
<button
|
|
1336
|
-
Clear finished</button>
|
|
1393
|
+
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
1394
|
+
<small class="text-muted">Auto-fetch workers feeding the cache.</small>
|
|
1395
|
+
<form hx-post="/admin/clear" hx-target="#dash" hx-swap="innerHTML" class="m-0">
|
|
1396
|
+
<button class="btn btn-sm btn-outline-secondary" type="submit"
|
|
1397
|
+
>Clear finished</button>
|
|
1337
1398
|
</form>
|
|
1338
1399
|
</div>
|
|
1339
|
-
<
|
|
1340
|
-
<thead><tr
|
|
1400
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1401
|
+
<thead class="table-light"><tr>
|
|
1402
|
+
<th>Artifact</th><th>Progress</th><th>Status</th><th></th>
|
|
1403
|
+
</tr></thead>
|
|
1341
1404
|
<tbody>{job_rows}</tbody>
|
|
1342
|
-
</table></
|
|
1405
|
+
</table></div>
|
|
1343
1406
|
</section>
|
|
1344
1407
|
|
|
1345
1408
|
<section id="tab-misses" class="tab{_active("tab-misses")}">
|
|
1346
|
-
<
|
|
1347
|
-
<thead><tr>
|
|
1409
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1410
|
+
<thead class="table-light"><tr>
|
|
1348
1411
|
<th>URL</th><th class="num">Misses</th><th>Last seen</th><th>Action</th>
|
|
1349
1412
|
</tr></thead>
|
|
1350
1413
|
<tbody>{miss_rows}</tbody>
|
|
1351
|
-
</table></
|
|
1414
|
+
</table></div>
|
|
1352
1415
|
</section>"""
|
|
1353
1416
|
|
|
1354
1417
|
def _stream_progress_cell(self, s: Stream) -> str:
|
|
@@ -1386,11 +1449,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1386
1449
|
cancel = ""
|
|
1387
1450
|
if j.status in PENDING_STATES:
|
|
1388
1451
|
cancel = (
|
|
1389
|
-
'<form hx-post="/admin/cancel" hx-target="#dash" hx-swap="innerHTML">'
|
|
1452
|
+
'<form hx-post="/admin/cancel" hx-target="#dash" hx-swap="innerHTML" class="m-0">'
|
|
1390
1453
|
f'<input type="hidden" name="id" value="{j.id}">'
|
|
1391
|
-
'<button
|
|
1454
|
+
'<button class="btn btn-sm btn-outline-secondary" type="submit"'
|
|
1455
|
+
">Cancel</button></form>"
|
|
1392
1456
|
)
|
|
1393
|
-
color = self.STATUS_COLORS.get(j.status, "
|
|
1457
|
+
color = self.STATUS_COLORS.get(j.status, "var(--bs-secondary)")
|
|
1394
1458
|
return f"""<tr>
|
|
1395
1459
|
<td class="url" title="{html.escape(j.url, quote=True)}">{html.escape(name)}</td>
|
|
1396
1460
|
<td>{prog}</td>
|