Compare commits

..
3 Commits
Author SHA1 Message Date
randy 750f6c8fb0 Silence the MultiJson deprecation warning in every test run
CI / deploy (push) Successful in 15s
CI / check (push) Successful in 44s
sinatra/json defaults its :json_encoder to MultiJson and calls
.encode on it, which newer multi_json versions deprecate in favor of
a constant this app has no other use for (sinatra-contrib 4.2.1, the
current release, still calls the old one -- not something fixable by
bumping a version). Every use of the json helper here is a plain
hash/array, so pointing :json_encoder at stdlib JSON instead sidesteps
multi_json entirely.
2026-09-07 23:33:52 +02:00
randy bf17e00463 Hide write-action controls in the tree UI when logged out
Delete (file and folder), download, upload, and new-folder were all
rendered unconditionally and relied on the backend's 401 + a redirect
to /login on click -- fine for surfacing the reason something failed,
but showing controls that plainly won't do anything until you log in
was the actual complaint. isLoggedIn() checks for the topbar's own
"Log out" link (only rendered when logged_in) instead of threading a
new flag through -- already there, and login state can't change
without a full page reload anyway.

Caught a real gap while verifying in a real browser: the folder row's
own delete button wasn't gated (only the file row's was), so a
"logged out" check still showed a delete icon on every folder. Fixed
before it shipped -- confirmed clean in both states afterward.
2026-09-07 23:31:45 +02:00
randy 058c0990b4 Document four more NPS-adjacent resources
- national-park-service/fonts: unofficial but genuinely open (SIL OFL
  1.1) typefaces inspired by public-lands signage -- unlike the
  proprietary Rawlinson/Frutiger this app already ruled out, this one
  has no licensing blocker if the look is ever wanted in this app's
  own UI chrome.
- The official NPS Data API (developer.nps.gov) -- a legitimate
  government data source, separate from the Mapbox-Atlas-licensed
  tile service. No current use case (this app isn't NPS-park-specific)
  but worth remembering.
- Two hobby projects (carlknutson/nps-changes-site,
  seankelliher/us-national-parks-alerts) noted as real-world examples
  of consuming that API, not as reusable libraries.
2026-09-07 23:21:37 +02:00
4 changed files with 87 additions and 10 deletions

No files matched your search

+7
View File
@@ -16,6 +16,13 @@ module Maptool
set :public_folder, File.join(__dir__, "public")
set :views, File.join(__dir__, "views")
# sinatra/json defaults to MultiJson, whose `encode` (what
# sinatra-contrib 4.2.1 actually calls) is deprecated in newer
# multi_json versions in favor of a constant this app has no other use
# for — stdlib JSON already does the same job and every use of the
# `json` helper below is a plain hash/array, nothing MultiJson-specific.
set :json_encoder, JSON
# A persistent secret across restarts/redeploys — otherwise every deploy
# would invalidate every logged-in session's cookie (see
# docs/deployment.adoc). Falls back to a random one so the app still
+9
View File
@@ -691,6 +691,15 @@ require login, guarded by `require_login!` in `app.rb` — and so does
exporting a file (`GET /api/gpx/file/*?download=...`), even though
plain viewing of that same URL (no `?download`) doesn't; not a write
exactly, but treated the same as the others rather than left open.
The tree UI (`public/js/gpx.js`) mirrors this: every control for a
gated action (delete, download, upload, new folder) is left out of
the DOM entirely when logged out rather than shown and failing on
click — `isLoggedIn()` checks for the topbar's own "Log out" link
(`views/index.erb` only renders one when `logged_in`) instead of
threading a separate flag through, since that's already there and
login state can't change without a full page reload anyway. Delete
exists on both file and folder rows — both are gated, easy to miss
one and only catch it by testing an actually-logged-out session.
Login delegates to the Gitea instance at `git.priesol.net` via OAuth2
(`GET /login` redirects to Gitea's authorize endpoint, `GET
+37
View File
@@ -246,6 +246,19 @@ own work (a map viewer, not a publisher of NPS-branded content):
Adobe Systems). Both proprietary/commercially licensed, unlike the
fonts this app already vendors (Noto Sans, SIL OFL — see
`docs/third-party-notices.adoc`) — not something to pull in.
* *A genuinely open alternative*: https://github.com/national-park-service/fonts
— despite the org name, its own `DISCLAIMER.md` is explicit that this
is an **independent, unofficial** project, "not affiliated with,
endorsed by, or sponsored by" NPS, and that its six typefaces (NPS
2026, Redwood Serif, Campmate Script, NPS Symbols, Sequoia Sans,
Switchback) are original works "inspired by" public-lands signage —
explicitly *not* reproductions of Rawlinson or anything NPS actually
owns. SIL OFL 1.1 licensed, same family of license as the fonts this
app already vendors. Unlike Rawlinson/Frutiger above, this one's
actually usable if the NPS-inspired look is ever wanted in this app's
own UI chrome (currently plain system fonts, `public/css/style.css`) —
no current use case, but no licensing blocker either, worth
remembering if that comes up.
* *Hawaiian typography*: NPS Rawlinson includes vowels with Hawaiian
macrons (ā, ē, ī, ō, ū) and the okina (ʻ); the regular weight of
Frutiger doesn't, so NPS uses a separate "HFrutiger" weight for
@@ -306,3 +319,27 @@ OSM-derived data, just a different source. No current need, and unlike
everything else in this doc (CC0, BSD, MIT, public domain) this repo has
**no `LICENSE` file at all** — its terms are genuinely unclear, so treat
that as a blocker too, not just "no use case yet."
=== NPS Data API (developer.nps.gov)
A separate thing from the tile/Park Tiles service above — a general
government open-data API (parks, alerts, campgrounds, places, ... —
official docs: https://www.nps.gov/subjects/developer/api-documentation.htm,
a JS-rendered Swagger UI that doesn't render via a plain fetch; a free
API key from that same page is required to call it). No current use
case (this app isn't NPS-park-specific — park4night POIs and personal
GPX/FIT tracks, not NPS units), but it's the legitimate, non-Mapbox-Atlas
government data source behind it, worth remembering if a "show nearby
NPS park info/alerts" feature ever comes up — the `wms`/`source-arcgis-rest`
npmap5-plugins entries above would be the MapLibre-side plumbing for it.
Two hobby projects (both MIT, not NPS-affiliated, not directly reusable
as libraries — noted only as real-world examples of consuming this API)
turned up while looking at this:
* https://github.com/carlknutson/nps-changes-site — a React/Leaflet
explorer for all NPS sites, most fields sourced from this API
(supplemented by manual/AI-assisted research for fields the API
doesn't have), data refreshed daily via GitHub Actions rather than
called live.
* https://github.com/seankelliher/us-national-parks-alerts — a
Vue + Express directory of park alerts, pulled live through this API.
+34 -10
View File
@@ -31,6 +31,17 @@ const BADGE_IMAGE_PREFIX = "badge-"; // one registered image per distinct badge
// one — both land on the same drop targets, told apart by this.
const MOVE_MIME = "application/x-gpx-path";
// No server-rendered flag for this exists yet, and adding one would be more
// plumbing than reusing what's already on the page: the topbar's own
// logged_in check (views/index.erb) already renders a "Log out" link only
// when logged in. Write-action controls (delete/download/upload/new
// folder) check this to hide themselves entirely rather than being shown
// and failing on click — login state never changes without a full page
// reload, so this only needs checking once per render, not reactively.
function isLoggedIn() {
return !!document.querySelector('a[href="/logout"]');
}
// A sidebar icon that opens a side panel for uploading GPX files (via
// drag & drop, stored server-side in a folder hierarchy the user can add to
// and reorganize by dragging), and toggling their tracks/waypoints on and
@@ -109,6 +120,10 @@ export class GpxPanelControl {
this._uploadStatus = this._panel.querySelector(".gpx-upload__status");
this._panel.querySelector('[data-action="new-folder"]').innerHTML = NEW_FOLDER_ICON;
this._panel.querySelector('[data-action="upload"]').innerHTML = UPLOAD_ICON;
if (!isLoggedIn()) {
this._panel.querySelector('[data-action="new-folder"]').hidden = true;
this._panel.querySelector('[data-action="upload"]').hidden = true;
}
this._panel
.querySelector(".gpx-panel__close")
.addEventListener("click", () => this._togglePanel(false));
@@ -249,7 +264,7 @@ export class GpxPanelControl {
const name = document.createElement("span");
name.textContent = folder.name;
row.appendChild(name);
row.appendChild(this._deleteButton(() => this._deleteFolder(folder.path)));
if (isLoggedIn()) row.appendChild(this._deleteButton(() => this._deleteFolder(folder.path)));
summary.appendChild(row);
this._wireFolderDropTarget(summary, folder.path);
@@ -277,34 +292,43 @@ export class GpxPanelControl {
// name reflects that (matching the real "<path>.geojson" cache file on
// disk) rather than still reading as a plain, un-converted .gpx/.fit.
// A direct .geojson upload already ends in .geojson, so it's left
// alone. It's an <a>, not a <span>: clicking it downloads that GeoJSON
// — the only way to get it out of the app, since the original .gpx/.fit
// alone.
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
const loggedIn = isLoggedIn();
// Logged in: an <a>, not a <span> — clicking it downloads that GeoJSON,
// the only way to get it out of the app, since the original .gpx/.fit
// is never deleted but also never re-shown once converted. An <a>
// nested in a <label> doesn't also toggle the checkbox on click (unlike
// a plain element), so this doesn't fight the row's own
// toggle-on-map behavior. href kept pointing at the real endpoint (so
// hover/right-click/middle-click still do something sensible) but the
// actual click goes through _download, which — like every other write
// action in this file — needs to redirect to /login on a 401 rather
// than navigate to a raw JSON error page (this endpoint now requires
// login the same as upload/move/delete, see app.rb).
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
const name = document.createElement("a");
// actual click goes through _download, which redirects to /login on a
// 401 rather than navigating to a raw JSON error page (this endpoint
// requires login the same as upload/move/delete, see app.rb).
// Logged out: plain text — nothing here would actually work, so it
// isn't shown as if it would (same reasoning as the buttons below).
const name = document.createElement(loggedIn ? "a" : "span");
name.className = "gpx-tree__file-name";
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
name.textContent = displayName;
if (loggedIn) {
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
name.title = `Download ${displayName}`;
name.addEventListener("click", (event) => {
event.preventDefault();
this._download(name.href, displayName);
});
}
label.appendChild(checkbox);
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
if (!file.cached) label.appendChild(this._pendingIcon());
label.appendChild(name);
row.appendChild(label);
// Delete/download require login (app.rb); shown only once logged in
// rather than shown-but-failing-on-click.
if (loggedIn) {
row.appendChild(this._downloadButton(file));
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
}
return row;
}