Show what's actually cached per-country, with a delete button per row
CI / check (push) Successful in 17s
CI / deploy (push) Successful in 13s

The only visibility into downloaded data was the whole offline.pmtiles
row, and the only delete was "everything" (whole file + whole cache).
GET /api/offline-tiles/cache lists what's really in
data/geofabrik_cache/ right now — distinct from offline.pmtiles' own
meta.sources, which only reflects the last successful build and can
disagree with both the current selection and the cache (ticked-but-
not-Applied, or built-from-but-since-unticked).

The region picker now shows each cached country's size next to its
checkbox, with its own delete button (DELETE
/api/offline-tiles/cache/:code, already added but unused by the UI
until now) — removing just that entry rather than clearing the whole
cache.
This commit is contained in:
randy committed 2026-09-11 23:39:43 +02:00
1 parent d09d953cbf
commit 78362d51f2
7 files changed
+158 -6

No files matched your search

+37
View File
@@ -416,11 +416,20 @@ html, body {
border-radius: 4px;
}
.offline-data-panel__region-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.offline-data-panel__region-list label {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
min-width: 0;
flex: 1;
cursor: pointer;
}
@@ -428,6 +437,34 @@ html, body {
background: #f0f0f0;
}
.offline-data-panel__cache-info {
display: flex;
align-items: center;
gap: 4px;
padding-right: 8px;
color: #888;
font-size: 11px;
white-space: nowrap;
}
.offline-data-panel__cache-delete {
border: none;
background: none;
padding: 0 2px;
color: #a33;
font-size: 11px;
cursor: pointer;
}
.offline-data-panel__cache-delete:hover {
text-decoration: underline;
}
.offline-data-panel__cache-delete:disabled {
opacity: 0.5;
cursor: default;
}
/* Same rounded-square badge ratio as icons.js's own MAP_MARKER_ICON
(rx 5 of a 22-wide square) — a consistent frame rather than however
each flag's own artwork happens to fill an unbordered square. */
+51 -5
View File
@@ -81,26 +81,72 @@ class RegionPicker {
container.appendChild(section);
let data;
let cache;
try {
data = await (await fetch("/api/offline-regions")).json();
[data, cache] = await Promise.all([
fetch("/api/offline-regions").then((r) => r.json()),
fetch("/api/offline-tiles/cache").then((r) => r.json()),
]);
} catch {
this._list.innerHTML = `<li class="offline-data-panel__region-status">Could not load the region list.</li>`;
return;
}
const selected = new Set(data.selected);
const cached = new Map(cache.extracts.map((e) => [e.code, e]));
this._list.innerHTML = data.available
.map(
(region) => `
.map((region) => {
const extract = cached.get(region.code);
// Downloaded-but-not-(yet)-built is a real state (ticked, not
// Applied yet) as is built-but-no-longer-selected (unticked since)
// — this badge reflects the cache on disk, independent of either.
const cacheInfo = extract
? `
<span class="offline-data-panel__cache-info">
${formatBytes(extract.size)}
${
isLoggedIn()
? `<button type="button" class="offline-data-panel__cache-delete" data-code="${escapeHtml(region.code)}" data-name="${escapeHtml(region.name)}" title="Delete downloaded data for ${escapeHtml(region.name)}">✕</button>`
: ""
}
</span>`
: "";
return `
<li>
<label>
<input type="checkbox" value="${escapeHtml(region.code)}" ${selected.has(region.code) ? "checked" : ""}>
<img class="offline-data-panel__flag" src="${flagIconUrl(region.code)}" alt="" width="18" height="18">
${escapeHtml(region.name)}
</label>
</li>`,
)
${cacheInfo}
</li>`;
})
.join("");
this._list.querySelectorAll(".offline-data-panel__cache-delete").forEach((button) => {
button.addEventListener("click", () => this._deleteCache(button));
});
}
// Removes just this row's cache badge on success, rather than re-fetching
// and re-rendering the whole list — that would also overwrite whatever
// unticked/ticked-but-not-yet-Applied state the user has pending.
async _deleteCache(button) {
const code = button.dataset.code;
if (!window.confirm(`Delete downloaded data for ${button.dataset.name}? This can't be undone.`)) return;
button.disabled = true;
try {
const response = await fetch(`/api/offline-tiles/cache/${code}`, { method: "DELETE" });
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `could not delete: ${response.status}`);
} catch (err) {
window.alert(err.message);
button.disabled = false;
return;
}
button.closest(".offline-data-panel__cache-info").remove();
}
async _apply() {