Track what was actually built, and flag when it's stale
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 16s

The .pmtiles files themselves carry no record of what upstream
snapshot they came from — only a local mtime. Each successful build
now writes a small sidecar (offline.pmtiles.meta.json: the Protomaps
build date used; terrain.pmtiles.meta.json: the Mapterhorn source
names/md5sums) recording that, surfaced in the panel next to the
existing size/updated line.

New GET .../check-update routes compare the recorded build against
the live upstream index/manifest — true/false/null, null meaning
"don't know" (no local build yet, or the check itself failed) rather
than a false "no". Kept separate from the plain status endpoints
(which map.js's own topbar dot hits on every page load) since this
one reaches an external service; only called once when the Offline
data panel is actually opened.

Also: flag emoji next to each country in the picker (derived from the
ISO code via Unicode regional indicators, no image assets needed).
This commit is contained in:
randy committed 2026-09-11 13:18:32 +02:00
1 parent 53c9a82af2
commit 4a3862189f
7 files changed
+261 -15

No files matched your search

+5
View File
@@ -387,6 +387,11 @@ class AppTest < Minitest::Test
assert_equal({ "state" => "idle" }, json_body(request.get("/api/terrain-tiles/build-status")))
end
def test_check_update_is_null_before_any_build
assert_nil json_body(request.get("/api/offline-tiles/check-update"))["update_available"]
assert_nil json_body(request.get("/api/terrain-tiles/check-update"))["update_available"]
end
def test_offline_tiles_build_is_409_with_no_selection
response = request.post("/api/offline-tiles/build", **logged_in_env)
assert_equal 409, response.status
+75 -4
View File
@@ -69,11 +69,11 @@ class PmtilesBuilderTest < Minitest::Test
# chunk-a covers Slovakia's real extent, chunk-b covers Iceland's —
# Spain falls in neither, for the "no chunk intersects" case.
def fake_manifest
def fake_manifest(chunk_a_md5: "md5-a")
[
{ "name" => "planet.pmtiles", "url" => "https://example.com/planet.pmtiles", "min_lon" => -180.0, "min_lat" => -85.0, "max_lon" => 180.0, "max_lat" => 85.0, "min_zoom" => 0, "max_zoom" => 12 },
{ "name" => "chunk-a.pmtiles", "url" => "https://example.com/chunk-a.pmtiles", "min_lon" => 16.0, "min_lat" => 47.0, "max_lon" => 23.0, "max_lat" => 50.0, "min_zoom" => 13, "max_zoom" => 15 },
{ "name" => "chunk-b.pmtiles", "url" => "https://example.com/chunk-b.pmtiles", "min_lon" => -25.0, "min_lat" => 63.0, "max_lon" => -13.0, "max_lat" => 67.0, "min_zoom" => 13, "max_zoom" => 15 },
{ "name" => "planet.pmtiles", "url" => "https://example.com/planet.pmtiles", "md5sum" => "md5-planet", "min_lon" => -180.0, "min_lat" => -85.0, "max_lon" => 180.0, "max_lat" => 85.0, "min_zoom" => 0, "max_zoom" => 12 },
{ "name" => "chunk-a.pmtiles", "url" => "https://example.com/chunk-a.pmtiles", "md5sum" => chunk_a_md5, "min_lon" => 16.0, "min_lat" => 47.0, "max_lon" => 23.0, "max_lat" => 50.0, "min_zoom" => 13, "max_zoom" => 15 },
{ "name" => "chunk-b.pmtiles", "url" => "https://example.com/chunk-b.pmtiles", "md5sum" => "md5-b", "min_lon" => -25.0, "min_lat" => 63.0, "max_lon" => -13.0, "max_lat" => 67.0, "min_zoom" => 13, "max_zoom" => 15 },
]
end
@@ -121,6 +121,40 @@ class PmtilesBuilderTest < Minitest::Test
refute_path_exists File.join(@data_dir, "offline.pmtiles.tmp")
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
assert_equal ["extract https://example.com/fake.pmtiles"], log_lines
assert_equal({ "source" => "fake.pmtiles" }, Maptool::PmtilesBuilder.offline_meta(@data_dir))
end
def test_offline_meta_is_nil_before_any_build
assert_nil Maptool::PmtilesBuilder.offline_meta(@data_dir)
end
def test_offline_update_available_is_nil_before_any_build
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
end
def test_offline_update_available_is_false_when_unchanged
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }).join
refute Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
end
def test_offline_update_available_is_true_when_a_newer_build_exists
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/old.pmtiles" }).join
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/new.pmtiles" })
end
def test_offline_update_available_is_nil_when_the_index_is_unreachable
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }).join
fetch_build_url = -> { raise Maptool::PmtilesBuilder::BuildError, "could not reach index" }
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: fetch_build_url)
end
def test_build_offline_reports_an_error_status_on_non_zero_exit
@@ -198,6 +232,43 @@ class PmtilesBuilderTest < Minitest::Test
merged = log_lines.select { |line| line.start_with?("merge ") }
assert_equal ["extract https://example.com/planet.pmtiles", "extract https://example.com/chunk-a.pmtiles"], extracted
assert_equal 1, merged.size
assert_equal(
{ "sources" => [{ "name" => "planet.pmtiles", "md5sum" => "md5-planet" }, { "name" => "chunk-a.pmtiles", "md5sum" => "md5-a" }] },
Maptool::PmtilesBuilder.terrain_meta(@data_dir),
)
end
def test_terrain_meta_is_nil_before_any_build
assert_nil Maptool::PmtilesBuilder.terrain_meta(@data_dir)
end
def test_terrain_update_available_is_false_when_unchanged
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
refute Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest })
end
# Same selection, but chunk-a's md5sum has since changed upstream.
def test_terrain_update_available_is_true_when_a_source_changed
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
assert Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest(chunk_a_md5: "md5-a-v2") })
end
# Same recorded sources, but the selection has since grown to also cover
# chunk-b — recorded sources no longer match what the current selection
# would need.
def test_terrain_update_available_is_true_when_the_selection_grew
select_regions("SK")
ENV["PMTILES_BIN"] = @pmtiles_ok
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
select_regions("SK", "IS")
assert Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest })
end
# Slovakia + Iceland together intersect both fake chunks — planet + both