withcache 0.6.3__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.3 → withcache-0.6.4}/PKG-INFO +1 -1
- {withcache-0.6.3 → withcache-0.6.4}/shim/build.zig.zon +1 -1
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/__init__.py +1 -1
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/server.py +192 -127
- 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.3/src/withcache/static/pico.min.css +0 -4
- {withcache-0.6.3 → withcache-0.6.4}/.gitignore +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/LICENSE +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/README.md +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/deploy/Containerfile +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/deploy/compose.yml +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/hatch_build.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/pyproject.toml +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/shim/build.zig +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/shim/shim.zig +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/_shim.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/client.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/oras.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/tests/test_differential.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/tests/test_oras.py +0 -0
- {withcache-0.6.3 → withcache-0.6.4}/tests/test_withcache.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>
|
|
@@ -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"]
|
|
@@ -58,7 +58,14 @@ USER_AGENT = f"withcache-cache/{__version__}"
|
|
|
58
58
|
# operating depth.
|
|
59
59
|
RESUME_MAX_ATTEMPTS = 5
|
|
60
60
|
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|
61
|
-
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
|
+
}
|
|
62
69
|
_DB_WRITE_LOCK = threading.Lock()
|
|
63
70
|
|
|
64
71
|
|
|
@@ -840,14 +847,24 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
840
847
|
return self.headers.get("HX-Request") == "true"
|
|
841
848
|
|
|
842
849
|
def serve_static(self, parsed):
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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("/"):
|
|
846
858
|
self.send_text(404, "not found\n")
|
|
847
859
|
return
|
|
848
|
-
|
|
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:
|
|
849
866
|
data = f.read()
|
|
850
|
-
ext = os.path.splitext(
|
|
867
|
+
ext = os.path.splitext(target)[1]
|
|
851
868
|
self.send_response(200)
|
|
852
869
|
self.send_header("Content-Type", MIME_TYPES.get(ext, "application/octet-stream"))
|
|
853
870
|
self.send_header("Content-Length", str(len(data)))
|
|
@@ -1012,88 +1029,143 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1012
1029
|
|
|
1013
1030
|
# -- HTML --------------------------------------------------------------
|
|
1014
1031
|
STATUS_COLORS: ClassVar[dict[str, str]] = {
|
|
1015
|
-
"queued": "
|
|
1016
|
-
"running": "var(--
|
|
1017
|
-
"completed": "
|
|
1018
|
-
"failed": "
|
|
1019
|
-
"cancelled": "
|
|
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)",
|
|
1020
1037
|
}
|
|
1021
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
|
+
|
|
1022
1051
|
def _head(self, title: str) -> str:
|
|
1052
|
+
primary = self._PRIMARY_HEX
|
|
1053
|
+
hover = self._PRIMARY_HOVER
|
|
1054
|
+
rgb = self._PRIMARY_RGB
|
|
1023
1055
|
return f"""<!doctype html>
|
|
1024
1056
|
<html lang="en"><head>
|
|
1025
1057
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1026
1058
|
<title>{title}</title>
|
|
1027
|
-
<link rel="stylesheet" href="/static/
|
|
1059
|
+
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
|
1060
|
+
<link rel="stylesheet" href="/static/bootstrap-icons.min.css">
|
|
1028
1061
|
<script src="/static/htmx.min.js"></script>
|
|
1029
1062
|
<style>
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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; }}
|
|
1033
1086
|
.url {{ word-break: break-all; }}
|
|
1034
1087
|
.num {{ text-align: right; }}
|
|
1035
|
-
.mono {{ font-family: var(--
|
|
1036
|
-
td form {{ display: inline; margin: 0; }}
|
|
1037
|
-
td button {{ width: auto; display: inline-block; margin: 0 .3rem 0 0;
|
|
1038
|
-
padding: .15rem .6rem; font-size: .8rem; }}
|
|
1039
|
-
td progress {{ margin: 0 0 .15rem; }}
|
|
1088
|
+
.mono {{ font-family: var(--bs-font-monospace); font-size: .85em; }}
|
|
1040
1089
|
#spin {{ width: 7rem; height: .5rem; margin: 0; }}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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; }}
|
|
1044
1102
|
</style>
|
|
1045
1103
|
</head>"""
|
|
1046
1104
|
|
|
1047
1105
|
def render_login(self, error: str = "") -> str:
|
|
1048
|
-
err = f'<div class="
|
|
1049
|
-
return f"""{self._head("withcache
|
|
1050
|
-
<body
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
<
|
|
1060
|
-
|
|
1061
|
-
|
|
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>
|
|
1062
1128
|
</main></body></html>"""
|
|
1063
1129
|
|
|
1064
1130
|
def render_page(self) -> str:
|
|
1065
1131
|
logout = (
|
|
1066
|
-
'<
|
|
1067
|
-
'<button type="submit" class="
|
|
1068
|
-
|
|
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>"
|
|
1069
1135
|
if self.auth.enabled
|
|
1070
1136
|
else ""
|
|
1071
1137
|
)
|
|
1072
1138
|
return f"""{self._head("withcache cache-host")}
|
|
1073
|
-
<body
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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>
|
|
1081
1150
|
{logout}
|
|
1082
|
-
</
|
|
1083
|
-
</
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
<
|
|
1087
|
-
|
|
1088
|
-
<
|
|
1089
|
-
<
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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>
|
|
1097
1169
|
|
|
1098
1170
|
<!-- The hx-trigger gates polling on the user NOT having an active
|
|
1099
1171
|
text selection, so highlight-and-copy a URL out of a table cell
|
|
@@ -1189,26 +1261,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1189
1261
|
# newly-inserted node is not the one ``:target`` resolved to
|
|
1190
1262
|
# at hash-change time). The visible symptom was the tab
|
|
1191
1263
|
# snapping back to Streams within a second of every click.
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
nav.tabs { margin: 1rem 0 .25rem; border-bottom: 1px solid var(--pico-muted-border-color); }
|
|
1195
|
-
nav.tabs ul { display: flex; gap: 0; padding: 0; margin: 0; list-style: none; }
|
|
1196
|
-
nav.tabs li { margin: 0; }
|
|
1197
|
-
nav.tabs a {
|
|
1198
|
-
display: inline-block; padding: .45rem .9rem; text-decoration: none;
|
|
1199
|
-
color: var(--pico-muted-color); border-bottom: 2px solid transparent;
|
|
1200
|
-
margin-bottom: -1px; font-size: .9rem;
|
|
1201
|
-
}
|
|
1202
|
-
nav.tabs a:hover { color: var(--pico-color); }
|
|
1203
|
-
nav.tabs a.active-tab {
|
|
1204
|
-
color: var(--pico-color);
|
|
1205
|
-
border-bottom-color: var(--pico-primary, #0172ad);
|
|
1206
|
-
font-weight: 600;
|
|
1207
|
-
}
|
|
1208
|
-
section.tab { display: none; padding-top: .75rem; }
|
|
1209
|
-
section.tab.active-tab { display: block; }
|
|
1210
|
-
</style>
|
|
1211
|
-
"""
|
|
1264
|
+
# The class hooks live in _head's <style> so this dash render
|
|
1265
|
+
# can be swapped 1 Hz without re-declaring them.
|
|
1212
1266
|
|
|
1213
1267
|
stream_rows = (
|
|
1214
1268
|
"".join(
|
|
@@ -1220,12 +1274,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1220
1274
|
</tr>"""
|
|
1221
1275
|
for s in streams
|
|
1222
1276
|
)
|
|
1223
|
-
or '<tr><td colspan="4"
|
|
1277
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1278
|
+
"<em>No active streams.</em></td></tr>"
|
|
1224
1279
|
)
|
|
1225
1280
|
|
|
1226
1281
|
job_rows = (
|
|
1227
1282
|
"".join(self._job_row(j) for j in jobs)
|
|
1228
|
-
or '<tr><td colspan="4"
|
|
1283
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1284
|
+
"<em>No downloads yet.</em></td></tr>"
|
|
1229
1285
|
)
|
|
1230
1286
|
|
|
1231
1287
|
miss_rows = (
|
|
@@ -1234,21 +1290,23 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1234
1290
|
<td class="url">{html.escape(m["url"])}</td>
|
|
1235
1291
|
<td class="num">{m["count"]}</td>
|
|
1236
1292
|
<td><small>{html.escape(m["last_seen"])}</small></td>
|
|
1237
|
-
<td>
|
|
1293
|
+
<td class="d-flex gap-2 flex-wrap">
|
|
1238
1294
|
<form hx-post="/admin/fetch" hx-target="#dash"
|
|
1239
|
-
hx-swap="innerHTML" hx-indicator="#spin">
|
|
1295
|
+
hx-swap="innerHTML" hx-indicator="#spin" class="m-0">
|
|
1240
1296
|
<input type="hidden" name="url" value="{html.escape(m["url"], quote=True)}">
|
|
1241
|
-
<button type="submit">Download</button>
|
|
1297
|
+
<button class="btn btn-sm btn-primary" type="submit">Download</button>
|
|
1242
1298
|
</form>
|
|
1243
|
-
<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">
|
|
1244
1300
|
<input type="hidden" name="key" value="{html.escape(m["key"], quote=True)}">
|
|
1245
|
-
<button
|
|
1301
|
+
<button class="btn btn-sm btn-outline-secondary" type="submit"
|
|
1302
|
+
>Dismiss</button>
|
|
1246
1303
|
</form>
|
|
1247
1304
|
</td>
|
|
1248
1305
|
</tr>"""
|
|
1249
1306
|
for m in misses
|
|
1250
1307
|
)
|
|
1251
|
-
or '<tr><td colspan="4"
|
|
1308
|
+
or '<tr><td colspan="4" class="text-center text-muted">'
|
|
1309
|
+
"<em>No misses recorded.</em></td></tr>"
|
|
1252
1310
|
)
|
|
1253
1311
|
|
|
1254
1312
|
# Build the per-row /b/ serve URL once; the cell wraps the
|
|
@@ -1274,19 +1332,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1274
1332
|
<td>{human_size(b["size"])}</td>
|
|
1275
1333
|
<td class="num">{b["hits"]}</td>
|
|
1276
1334
|
<td class="num">{b["misses"]}</td>
|
|
1277
|
-
<td class="mono">{html.escape(b["sha256"][:12])}
|
|
1335
|
+
<td class="mono">{html.escape(b["sha256"][:12])}...</td>
|
|
1278
1336
|
<td><small>{html.escape(b["fetched_at"])}</small></td>
|
|
1279
1337
|
<td>
|
|
1280
1338
|
<form hx-post="/admin/delete" hx-target="#dash" hx-swap="innerHTML"
|
|
1281
|
-
hx-confirm="Delete this cached artifact?">
|
|
1339
|
+
hx-confirm="Delete this cached artifact?" class="m-0">
|
|
1282
1340
|
<input type="hidden" name="key" value="{html.escape(b["key"], quote=True)}">
|
|
1283
|
-
<button
|
|
1341
|
+
<button class="btn btn-sm btn-outline-danger" type="submit"
|
|
1342
|
+
>Delete</button>
|
|
1284
1343
|
</form>
|
|
1285
1344
|
</td>
|
|
1286
1345
|
</tr>"""
|
|
1287
1346
|
for b in blobs
|
|
1288
1347
|
)
|
|
1289
|
-
or '<tr><td colspan="7"
|
|
1348
|
+
or '<tr><td colspan="7" class="text-center text-muted">'
|
|
1349
|
+
"<em>Cache is empty.</em></td></tr>"
|
|
1290
1350
|
)
|
|
1291
1351
|
|
|
1292
1352
|
# Per-tab counts let the operator see at a glance whether each
|
|
@@ -1295,59 +1355,63 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1295
1355
|
njobs = len(jobs)
|
|
1296
1356
|
|
|
1297
1357
|
return f"""
|
|
1298
|
-
<p
|
|
1299
|
-
{
|
|
1300
|
-
<nav class="tabs"
|
|
1301
|
-
<
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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>
|
|
1310
1372
|
|
|
1311
1373
|
<section id="tab-cached" class="tab{_active("tab-cached")}">
|
|
1312
|
-
<
|
|
1313
|
-
<thead><tr>
|
|
1374
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1375
|
+
<thead class="table-light"><tr>
|
|
1314
1376
|
<th>URL</th><th>Size</th><th class="num">Hits</th><th class="num">Misses</th>
|
|
1315
1377
|
<th>SHA-256</th><th>Fetched</th><th>Action</th>
|
|
1316
1378
|
</tr></thead>
|
|
1317
1379
|
<tbody>{blob_rows}</tbody>
|
|
1318
|
-
</table></
|
|
1380
|
+
</table></div>
|
|
1319
1381
|
</section>
|
|
1320
1382
|
|
|
1321
1383
|
<section id="tab-streams" class="tab{_active("tab-streams")}">
|
|
1322
|
-
<
|
|
1323
|
-
<thead><tr>
|
|
1384
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1385
|
+
<thead class="table-light"><tr>
|
|
1324
1386
|
<th>URL</th><th>Client</th><th>Progress</th><th>Age</th>
|
|
1325
1387
|
</tr></thead>
|
|
1326
1388
|
<tbody>{stream_rows}</tbody>
|
|
1327
|
-
</table></
|
|
1389
|
+
</table></div>
|
|
1328
1390
|
</section>
|
|
1329
1391
|
|
|
1330
1392
|
<section id="tab-downloads" class="tab{_active("tab-downloads")}">
|
|
1331
|
-
<div class="
|
|
1332
|
-
<small>Auto-fetch workers feeding the cache.</small>
|
|
1333
|
-
<form hx-post="/admin/clear" hx-target="#dash" hx-swap="innerHTML"
|
|
1334
|
-
<button
|
|
1335
|
-
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>
|
|
1336
1398
|
</form>
|
|
1337
1399
|
</div>
|
|
1338
|
-
<
|
|
1339
|
-
<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>
|
|
1340
1404
|
<tbody>{job_rows}</tbody>
|
|
1341
|
-
</table></
|
|
1405
|
+
</table></div>
|
|
1342
1406
|
</section>
|
|
1343
1407
|
|
|
1344
1408
|
<section id="tab-misses" class="tab{_active("tab-misses")}">
|
|
1345
|
-
<
|
|
1346
|
-
<thead><tr>
|
|
1409
|
+
<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0">
|
|
1410
|
+
<thead class="table-light"><tr>
|
|
1347
1411
|
<th>URL</th><th class="num">Misses</th><th>Last seen</th><th>Action</th>
|
|
1348
1412
|
</tr></thead>
|
|
1349
1413
|
<tbody>{miss_rows}</tbody>
|
|
1350
|
-
</table></
|
|
1414
|
+
</table></div>
|
|
1351
1415
|
</section>"""
|
|
1352
1416
|
|
|
1353
1417
|
def _stream_progress_cell(self, s: Stream) -> str:
|
|
@@ -1385,11 +1449,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1385
1449
|
cancel = ""
|
|
1386
1450
|
if j.status in PENDING_STATES:
|
|
1387
1451
|
cancel = (
|
|
1388
|
-
'<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">'
|
|
1389
1453
|
f'<input type="hidden" name="id" value="{j.id}">'
|
|
1390
|
-
'<button
|
|
1454
|
+
'<button class="btn btn-sm btn-outline-secondary" type="submit"'
|
|
1455
|
+
">Cancel</button></form>"
|
|
1391
1456
|
)
|
|
1392
|
-
color = self.STATUS_COLORS.get(j.status, "
|
|
1457
|
+
color = self.STATUS_COLORS.get(j.status, "var(--bs-secondary)")
|
|
1393
1458
|
return f"""<tr>
|
|
1394
1459
|
<td class="url" title="{html.escape(j.url, quote=True)}">{html.escape(name)}</td>
|
|
1395
1460
|
<td>{prog}</td>
|