Fix Planetiler tmp-file format detection, add build progress and delete
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 13s

Root cause of "planetiler exited with status 1: ... Unsupported format
tmp" (and likely the earlier unexplained "pmtiles exited with status
1"): Planetiler/go-pmtiles pick their output archive format from the
file extension, and the atomic-rename tmp files were named
"offline.pmtiles.tmp"/"terrain.pmtiles.tmp" — ".tmp" as the extension,
not ".pmtiles". Renamed to "offline.tmp.pmtiles"/"terrain.tmp.pmtiles".

Also, since build-status only ever said "running since X" with no way
to tell what it was actually doing:
- GET .../build-status now includes a `step` string ("fetching PL
  (3/4) — done: SK, CZ — pending: HU", "tiling with planetiler", ...)
  while a build is in progress.
- DELETE /api/offline-tiles / /api/terrain-tiles clear a built file,
  its sidecar meta, and (for offline) the whole per-country download
  cache — a manual "reclaim space" / "start over" action the region
  picker's Apply never did on its own. DELETE
  /api/offline-tiles/cache/:code clears just one country's cached
  extract. All three are 409 while that kind's build is running, and
  surfaced as a small "Delete" button on each row in the Offline data
  panel.
This commit is contained in:
randy committed 2026-09-11 23:23:46 +02:00
1 parent 2878270187
commit b54020e23b
7 files changed
+327 -23

No files matched your search

+27
View File
@@ -472,6 +472,33 @@ html, body {
border-top: 1px solid #eee;
}
.offline-data-panel__row-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.offline-data-panel__delete {
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
padding: 2px 8px;
font: inherit;
font-size: 11px;
color: #a33;
cursor: pointer;
}
.offline-data-panel__delete:hover {
background: #fdecec;
}
.offline-data-panel__delete:disabled {
opacity: 0.5;
cursor: default;
}
.offline-data-panel__status {
margin: 6px 0 0;
color: #666;
+41 -6
View File
@@ -17,12 +17,14 @@ function formatElapsed(ms) {
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
}
// "Building…" alone gave no way to tell a slow build from a stuck one —
// elapsed time answers "is it alive" regardless of how long a given
// selection takes.
// "Building…" alone gave no way to tell a slow build from a stuck one, or
// which of the current selection's sources it's actually working on right
// now — elapsed time answers "is it alive", status.step (see
// PmtilesBuilder.run_job) answers "doing what, and what's left".
function formatRunning(status) {
const elapsed = status.started_at ? ` (${formatElapsed(Date.now() - new Date(status.started_at).getTime())})` : "";
return `Building…${elapsed}`;
const step = status.step ? `: ${status.step}` : "";
return `Building…${elapsed}${step}`;
}
// Vendored square-cropped SVGs (public/vendor/flag-icons/, MIT, see
@@ -131,9 +133,10 @@ class RegionPicker {
// one class covers both rather than writing the same fetch/poll logic
// twice.
class DataFileRow {
constructor({ label, statusUrl, buildUrl, buildStatusUrl, checkUpdateUrl, formatMeta }) {
constructor({ label, statusUrl, deleteUrl, buildUrl, buildStatusUrl, checkUpdateUrl, formatMeta }) {
this._label = label;
this._statusUrl = statusUrl;
this._deleteUrl = deleteUrl;
this._buildUrl = buildUrl;
this._buildStatusUrl = buildStatusUrl;
this._checkUpdateUrl = checkUpdateUrl;
@@ -144,10 +147,15 @@ class DataFileRow {
const row = document.createElement("div");
row.className = "offline-data-panel__row";
row.innerHTML = `
<strong>${this._label}</strong>
<div class="offline-data-panel__row-head">
<strong>${this._label}</strong>
<button type="button" class="offline-data-panel__delete" hidden>Delete</button>
</div>
<p class="offline-data-panel__status">Checking…</p>
`;
this._statusEl = row.querySelector(".offline-data-panel__status");
this._deleteButton = row.querySelector(".offline-data-panel__delete");
this._deleteButton.addEventListener("click", () => this._delete());
container.appendChild(row);
this._refreshStatus();
}
@@ -163,14 +171,39 @@ class DataFileRow {
if (!data.available) {
this._statusEl.textContent = "Not generated yet.";
this._deleteButton.hidden = true;
return;
}
const updated = new Date(data.mtime).toLocaleString([], { dateStyle: "short", timeStyle: "short" });
this._statusEl.textContent = `${formatBytes(data.size)} · updated ${updated}${this._formatMeta(data.meta)}`;
this._deleteButton.hidden = !isLoggedIn();
this._checkUpdate();
}
// Deletes the built file (and, for offline.pmtiles, its whole per-country
// download cache — see PmtilesBuilder.delete_offline_data) so the row
// goes back to "Not generated yet." — a manual way to reclaim space or
// force a completely fresh rebuild, since Apply alone never removes
// anything already on disk.
async _delete() {
if (!window.confirm(`Delete ${this._label.toLowerCase()}? This can't be undone.`)) return;
this._deleteButton.disabled = true;
try {
const response = await fetch(this._deleteUrl, { 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);
return;
} finally {
this._deleteButton.disabled = false;
}
this._refreshStatus();
}
// Separate from _refreshStatus's own fetch (and only called once, right
// after — not polled): this one reaches an external service (Geofabrik
// or Mapterhorn's manifest, see lib/maptool/pmtiles_builder.rb), so it's
@@ -249,6 +282,7 @@ export class OfflineDataControl {
new DataFileRow({
label: "Offline map data",
statusUrl: "/api/offline-tiles",
deleteUrl: "/api/offline-tiles",
buildUrl: "/api/offline-tiles/build",
buildStatusUrl: "/api/offline-tiles/build-status",
checkUpdateUrl: "/api/offline-tiles/check-update",
@@ -257,6 +291,7 @@ export class OfflineDataControl {
new DataFileRow({
label: "Offline terrain data",
statusUrl: "/api/terrain-tiles",
deleteUrl: "/api/terrain-tiles",
buildUrl: "/api/terrain-tiles/build",
buildStatusUrl: "/api/terrain-tiles/build-status",
checkUpdateUrl: "/api/terrain-tiles/check-update",