Commit Graph
55 Commits
Author SHA1 Message Date
randy 88637ded22 Move the locate-me button into the top-left control stack
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
GeolocateControl was alone at bottom-right while Home/zoom/base-map were
grouped top-left — split across two corners instead of one stack like
the National Park Service's own map controls. Moved it in, between zoom
and the base-map button.
2026-09-07 20:37:43 +02:00
randy 8fa3391c6b Add an Alt+click debug inspector, NPMap4-infobox style
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 13s
NPMap4 has infoboxControl: a generic "click anything, show every raw
property" debug view, kept separate from a dataset's own curated popup.
Not portable code (Leaflet), but the concept is: Alt+click anywhere now
opens a popup listing every rendered feature at that point via
map.queryRenderedFeatures — base map OSM tags included, not just this
app's own GPX/FIT/radar layers — with a raw key/value table per feature.

No dedicated UI for it on purpose: a keyboard-modified click needs no
button, toggle state, or cursor change of its own, and it's fine for it to
coexist with a layer's own click handler firing on the same click (e.g. a
GPX waypoint's normal popup) since this is a power-user/debugging
affordance, not a replacement for those. Feature properties can come from
an uploaded GPX/FIT file (untrusted input) via this app's own layers, so
every value is escaped the same way gpx.js's popups already are.
2026-09-07 20:34:15 +02:00
randy d58580f4d9 Document national-dataset-query (not adopted)
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 14s
A CLI tool for pulling ArcGIS/ESRI REST data into GeoJSON/GeoPackage -
different category from the browser plugins/icons elsewhere in this doc.
No current use case (we have no ArcGIS data source to query) and,
unlike everything else surveyed so far, no LICENSE file at all.
2026-09-07 20:27:16 +02:00
randy ebc19cb46f Note that NPMap4 is currently more feature-complete than npmap5
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 20s
v4 is one mature, accumulated framework; npmap5 is a still-in-progress
rewrite split into small, independently-versioned (mostly pre-1.0)
plugins, several of which aren't merged yet. Worth knowing so we don't
assume v5 already has everything v4 does.
2026-09-07 20:24:38 +02:00
randy 215957382a Group Home/zoom/base-map into one top-left stack, NPS-style
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
Home was alone at top-left and zoom (NavigationControl) was down at
bottom-right with GeolocateControl — split across two corners instead of
grouped together like the National Park Service's own map controls
(https://www.nps.gov/, screenshot: Home, zoom in/out, base map, all
stacked in one place).

Moved NavigationControl up to join Home (showCompass: false — this map
never rotates, so a bearing-reset button would be dead weight), and added
MapLayersButton, a small map-corner control that's a second entry point
into the same Online/Offline panel LayerToggleControl already manages in
the page's sidebar (both go through the same toggle() now, so there's one
source of truth for whether the panel's open). GeolocateControl stays
alone at bottom-right — it's a different action (go to my actual location)
from Home (reset to the app's default view), which was worth clarifying
since the two are easy to conflate.
2026-09-07 20:23:37 +02:00
randy be6d61e136 Stop coloring forests/scrub the same green as parks
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
The previous pass applied Park Tiles' real park/protected-area green
(#b3e378) to wood_a/wood_b/scrub_a/scrub_b as well as park_a/park_b —
but in Park Tiles' own Imagery style, that green is specifically the
protected-area/administrative color; actual vegetation there is just the
aerial photo underneath, which this offline style has no fill equivalent
of. Applying it everywhere made the whole map read as green instead of
"brown with green parks popping out", which is the actual character of
their palette. Forests/scrub now render in muted brown/khaki instead,
leaving the vivid green for park_a/park_b alone.
2026-09-07 20:17:09 +02:00
randy a8f657b83c Document NPS typography (Rawlinson/Frutiger) and editorial style guide
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 14s
Noted for completeness in docs/nps-resources.adoc — both proprietary/
licensed fonts, unlike the SIL OFL Noto Sans already vendored, so not
adoptable. The editorial style guide is NPS's own writing/terminology
conventions, not a design or code resource, included only since it was
shared alongside the typography notes.
2026-09-07 20:13:06 +02:00
randy bc505436c1 Document all NPS mapping resources investigated this session
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 16s
A reference doc (docs/nps-resources.adoc) covering everything looked at
while pulling icons/controls/theme colors from NPS's mapping ecosystem:
what's actually in use (symbol-library, home-button, the Park Tiles-inspired
offline palette), what was considered and deliberately skipped (svg-icons,
markdown-handlebars, the unmerged overview/carto/wms branches) and why, the
Park Tiles cartography research (Harpers Ferry Center lineage, the four
style variants, why the actual tile service is off-limits via Mapbox
Atlas licensing, the public style JSON and Mapbox Studio style IDs found),
and the still-open upstream PR revising artwork for three icons we use.

Worth having as a knowledge base rather than re-deriving any of this from
scratch next time — NPS's tooling has repeatedly turned out to be a better
source than building something from nothing.
2026-09-07 20:07:27 +02:00
randy a95964db77 Recolor the offline theme towards NPS Park Tiles' palette
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 13s
Extended OFFLINE_THEME's color-role overrides to lean into the warm,
Harpers-Ferry-Center-derived look of the National Park Service's own "Park
Tiles" cartography, rather than the cooler default protomaps-themes-base
palette. A handful of values (background, the park/protected-area green, the
two road-importance tiers, the park-boundary line color) are the genuine
paint colors pulled from a public Park Tiles style JSON — the "Imagery"
variant is the only one of NPS's four styles served from a plain,
unauthenticated URL; "Standard"/"Slate"/"Light" resolve through Mapbox
Studio's private API with an access token, which isn't something to
piggyback on even though it's technically visible in their own JS bundle.
The rest of the palette (forest/scrub greens, water, sand, buildings) is
reasoned to match that same family rather than sourced.

Recreating the color choices is fine — that's not the kind of thing a data
license restricts. What stays off-limits is their actual tile service: its
OSM/terrain layers are licensed through Mapbox Atlas, a commercial,
non-sublicensable data service scoped to NPS's own use.
2026-09-07 20:00:50 +02:00
randy 170af80cac Add a Home button and fill icon gaps from NPS's newer symbol library
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 18s
Two things pulled in from github.com/nationalparkservice/npmap5-plugins:

- A "Home" button (public/vendor/npmap-home-button/, NPS's own MapLibre
  control) pinned to the app's CENTER/ZOOM, sitting top-left on the map. Now
  that the URL hash permalink can open the map at any view, "current view"
  and "the default view" aren't the same thing anymore, so a quick way back
  to the default is worth having.

- npmap5-plugins bundles a newer, actively-maintained version of the same
  NPS symbol-library already used for POI_ICONS (confirmed by its
  maintainer to be the designated replacement for the older standalone
  repo: nationalparkservice/npmap5-plugins#4), with a much larger icon
  vocabulary and a simpler CC0 (public domain) license. All 25 icons
  already in use came back byte-identical from the new source, so this
  just repoints their attribution — but it also has icons the old repo
  didn't: `forest` (POI_ICONS) is now the official "wilderness" pictogram
  instead of hand-drawn, and three of the five previously icon-less
  Services/Activities tags (wifi, mobile data coverage, winter/snow
  camping) now have real icons too. Only bakery and vehicle wash still
  have no NPS equivalent.
2026-09-07 19:45:19 +02:00
randy 481233c232 Hide the Base map switch when there's no offline map to switch to
CI / check (push) Successful in 46s
CI / deploy (push) Successful in 19s
The Online/Offline sidebar icon was always shown, even on an install with
no data/offline.pmtiles — offering a choice that can't actually work.
LayerToggleControl#remove() removes the icon (and hides its panel) once the
existing /api/offline-tiles check confirms there's nothing to switch to; a
transient failure of that same-origin check leaves the switch as is rather
than guessing, since that's the request itself failing, not evidence
offline tiles are unavailable.
2026-09-07 17:32:05 +02:00
randy fd8852c0e1 Fix meta-row color regression, show Services/Activities as icon tags
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 15s
The gray color from the previous commit landed on .gpx-popup__meta (the
whole row, label and value both) instead of just .gpx-popup__meta-label —
only the label was supposed to be muted, the value should read as normal
text same as the description above it.

While in there: Services/Activities were showing park4night's own raw,
comma-separated codes ("animaux, point_eau, wc_public"), which don't mean
anything to a reader on their own. PARK4NIGHT_TAGS (icons.js) maps the
codes p4n2gpx's exports actually use to a human label and, where the
National Park Service's icon set (already used for POI_ICONS) has a
reasonable match, a small icon — formatTags (gpx.js) renders each as an
icon+label tag instead of raw text. A code not in that table just falls
back to its own raw text, so nothing is ever hidden.
2026-09-07 17:25:37 +02:00
randy 152a08f922 Sync the map view to the URL, like Google Maps/OSM permalinks
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 19s
Panning/zooming now updates #map=<zoom>/<lat>/<lon> in the URL, and loading
a URL with one already opens at that view instead of the default center —
so copying the URL bar shares the exact spot someone's looking at, and a
page reload doesn't lose the current view either.

This is MapLibre GL JS's own built-in Hash control (hash: "map" on the Map
constructor) — it already does exactly this, replaceState and all, no
custom code needed.
2026-09-07 17:12:21 +02:00
randy 4447b6cebb Make Rating/Services/Activities in waypoint popups muted, not bold
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 13s
Secondary metadata, not something worth visually competing with the actual
description — dropped the bold label weight and colored the whole meta
block the same muted gray already used for category/time elsewhere in the
popup.
2026-09-07 17:07:25 +02:00
randy 3759f7a5c9 Format the structured lines in park4night waypoint popups
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 16s
A park4night <desc> packs a free-text narrative and several lines of
structured data (address, rating, services, activities, the listing URL)
into one field, separated only by newlines. Rendered as a single paragraph
it read as a wall of run-together text — no visual hierarchy between the
actual description and what's really key-value metadata.

formatDescription now recognizes "Label: value" lines (Rating/Services/
Activities) and bare-URL lines, and gives each its own row instead of
burying them in the narrative paragraph: a bordered meta block with bold
labels, and an isolated link line. Anything that doesn't match — the real
narrative text, or a plain GPX file's <desc> with no such structure — stays
exactly as it rendered before, so this only adds structure, never removes
or reorders text.
2026-09-07 17:03:36 +02:00
randy 6f7d332993 Stop faking SDF for POI icons, fix the resulting blur
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 14s
The badge and category glyphs were registered as SDF images (a single alpha
shape MapLibre tints at render time via icon-color) purely so one shared
sprite could be recolored per category. But SDF rendering assumes the
source actually encodes a distance field — a smooth multi-pixel gradient at
every edge — and these were just normally antialiased rasters (a sharp ~1px
transition). MapLibre's SDF antialiasing pass softened every edge as a
result, visibly blurrier than the base map's own icons, worst on the small
category glyph.

The glyph color never varies (always white), so it never needed runtime
tinting at all: _registerCategoryGlyphs now bakes #fff directly into the
raster and registers one plain image per category. The badge color does
vary (arbitrary per-file hex), but a real POI export only has a handful of
distinct category colors, so _registerBadgeColors rasterizes one sharp
image per distinct color actually present in the points being shown,
cached by color, instead of one shared image tinted at render time.
2026-09-07 16:55:23 +02:00
randy 7de8428b05 Swap the pin marker for a rounded-square badge
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 14s
The National Park Service frames its own map symbols (the icons already
vendored in for POI categories) in a rounded square, not a pin — pairing a
pin shape with NPS-style pictograms looked like two mismatched icon
conventions. Switched the marker to a rounded square to match.

This also simplifies the placement code: a pin has to anchor at its tip, so
the category glyph on top needed an offset computed from the pin/glyph
geometry to land in the pin's head. A square has no tip — both the badge and
the glyph layer just center directly on the coordinate with no offset math
at all.
2026-09-07 16:45:22 +02:00
randy 2a83bb10ca Turn POI markers into pins and use a real icon set
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
Colored circles for waypoints looked too similar to a base map style's own
small circular POI icons, so uploaded POI data blended into the map instead
of standing out. Switched the marker shape to a pin/teardrop (reusing the
existing WAYPOINT_ICON path, filled instead of stroked) — no base map style
uses that shape for its own points, so it reads immediately as "your data".
Anchored at its tip, the standard map-pin convention, rather than centered
on the coordinate.

Also swapped the hand-drawn category glyphs for the National Park Service's
own map symbols (github.com/nationalparkservice/symbol-library, BSD
licensed, forked from Mapbox's Maki) for the six categories it covers —
professional, purpose-built pictograms beat ad-hoc "fuel pump"/"picnic
table" line art. `forest` has no NPS equivalent and stays hand-drawn.

The glyph now sits in the pin's head rather than centered on a flat dot,
which needed real geometry (both layers anchor to the same point, so the
offset is the gap between where the pin's head-center ends up and where the
glyph's own visual center already sits) rather than the old anchor="center"
zero-offset placement.
2026-09-07 16:37:31 +02:00
randy 0b18dc018a Color-code, cluster and add icons to POI waypoints
CI / check (push) Successful in 47s
CI / deploy (push) Successful in 16s
A park4night/OsmAnd POI export (e.g. a p4n2gpx area export) tags every
waypoint with a category and a per-category color/icon in its <extensions>
block, all of which was previously thrown away — every waypoint rendered as
the same plain red dot, unreadable once a file has a few thousand of them.

Maptool::Gpx now captures category/color/icon (waypoint_style), matched by
namespace URI rather than local element name: a p4n2gpx file carries both
<osmand:icon> and <locus:icon> side by side, and REXML strips prefixes from
Element#name, so both report as just "icon" — matching on that alone was
silently picking the (useless) Locus drawable path instead of the OsmAnd
category key. Regression test added for this.

On the map: waypoints are colored by category (falling back to the plain
track color for anything uncategorized), a small hand-drawn icon set
(public/js/icons.js's POI_ICONS, rasterized to SDF sprites at runtime) draws
a glyph on top for the categories it covers, the points source is clustered
so a dense export collapses into count bubbles instead of a solid blob, and
a legend in the side panel lists whichever categories are currently visible.

Splitting each file's GeoJSON into a lines source and a points source
(rather than one mixed source with a geometry-type filter per layer) was
needed for clustering, since MapLibre's cluster support only understands a
source made entirely of points.
2026-09-07 16:18:06 +02:00
randy 708c08259b Fix a severe GPX parse slowdown and add route (<rte>) support
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 16s
GPX tracks were silently failing to render for large real-world files:
point_extensions used REXML's "extensions//*" XPath, and the // descendant
axis costs low-single-digit milliseconds per call regardless of subtree
size. Run once per point, that's negligible on small files but ~30s on a
real 8000-point track — long enough to hit a proxy timeout in production,
so the file just never appeared with no error. Replaced with plain
recursive child traversal (~40x faster on the same file, confirmed
29.9s -> 3.16s); test_parsing_many_points_with_extensions_stays_fast
guards against reintroducing it.

Also adds support for <rte> routes (not just <trk> tracks and <wpt>
waypoints), sharing the track/route conversion logic via a new
line_feature helper. Routes get their own "route" kind and a dashed
ROUTE_ICON in the file tree so the type is visible at a glance.
2026-09-07 14:36:12 +02:00
randy 0394befdec Ignore tea CLI's local log cache (.tmp/gitea-logs)
CI / check (push) Successful in 54s
CI / deploy (push) Successful in 28s
2026-09-07 14:19:34 +02:00
randy 11fc14824e Surface GPX/FIT parse errors instead of silently rendering nothing
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 17s
_addLayers handed MapLibre a URL and let it fetch the GeoJSON
internally — a 422 (a file REXML/Fit can't parse) just meant the
layer never appeared, with nothing telling the user why. It now
fetches the file itself first, alerts with the actual error and
un-checks the box on failure.
2026-09-07 14:10:46 +02:00
randy a0476f1204 Add a Distance/Time toggle to the profile chart's x-axis
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
TrackStats.build_profile now also computes elapsed_secs per point
(relative to the first known timestamp), alongside the existing
cumulative distance. Point x-positions stay distance-based either
way — only the shared axis strip's 5 tick labels (and the tooltip)
switch between the two, so toggling is a cheap re-render rather than
rebuilding the charts. The toggle only appears when a track actually
has elapsed-time data to switch to.
2026-09-07 13:54:49 +02:00
randy 4f7fe3c2a4 Show min/max y-axis values on each profile chart
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 15s
Plain HTML overlays, not SVG text: the chart's viewBox scales width
and height independently (preserveAspectRatio="none", for a
responsive width at a fixed pixel height), so text drawn inside that
coordinate space would get stretched by the mismatched factor.
2026-09-07 13:44:21 +02:00
randy 105651f005 Add activity type and a click-to-open track profile panel
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 15s
Track properties gain activity_type (FIT: sport enum from the
session/sport message, Garmin's own table; GPX: <trk><type> passed
through as-is) alongside the existing distance/elevation/duration.

Clicking a track line now opens a bottom panel with a chart per
metric the track actually has (elevation always; speed/cadence/heart
rate only when present — FIT's record fields or GPX's device
extensions). TrackStats.build_profile computes cumulative distance
the same way for both formats so they're directly comparable. One
pointermove listener drives a synchronized crosshair across every
chart, a single tooltip listing all series, and a map marker at the
hovered point's position — hovering the chart shows where on the
track that moment was.
2026-09-07 13:38:49 +02:00
randy cd4eef342b Add a persisted test suite
CI / check (push) Successful in 1m56s
CI / deploy (push) Successful in 2m56s
`bundle exec rake test` (minitest, a default gem but pinned in
Gemfile for reproducibility) now covers what ad-hoc scripts kept
re-verifying by hand during development: Gpx/Fit/TrackStats parsing,
GpxStore/LiveTrackStore file/TTL logic, LiveTrackClient's payload
parsing, and app.rb's routes end to end via Rack::MockRequest.

MAPTOOL_DATA_DIR (new, app.rb) lets the suite point GPX_STORE/
LIVETRACK_STORE at a throwaway directory instead of real data/ —
without it every run would leave uploaded test files behind. Each
lib file's old `if __FILE__ == $0` self-check is now redundant with
real tests covering the same ground, so removed rather than kept as
a second copy of the same assertions. Wired into ci.yml so it isn't
just a local convenience.
2026-09-07 12:46:35 +02:00
randy a3c31ace06 Show track properties (distance, elevation gain, duration)
CI / check (push) Successful in 38s
CI / deploy (push) Successful in 17s
Maptool::TrackStats computes these once, shared by Gpx and Fit so
both track feature shapes carry the same properties regardless of
source format. Hovering a track line (not clicking — these describe
the whole line, not one point) shows them in a popup, same mechanism
GPX waypoints already use.
2026-09-07 12:38:30 +02:00
randy fc174d4267 Support uploading FIT files (and zipped exports) alongside GPX
CI / check (push) Successful in 2m11s
CI / deploy (push) Successful in 2m27s
Maptool::Fit reads Garmin's binary FIT format directly rather than
adding a gem — the one Ruby option depends on activesupport, unmaintained
since 2022, for a single-purpose binary parser. Only "record" messages
are read (position + altitude; everything else is skipped by byte
length, never interpreted); field numbers and scale/offset are
Garmin's own (fit-python-sdk's profile.py), and the framing plus all
extracted values were checked byte-for-byte against 5 real device
files from python-fitparse's test suite before this shipped.

A .zip upload (Garmin's bulk/FIT export format) is expanded into its
.gpx/.fit entries via rubyzip — that one's an actual dependency, since
ZIP's own format isn't the kind of narrow, single-purpose parsing FIT
above is.
2026-09-07 12:23:37 +02:00
randy 8d3c126a14 Auto-refresh a track while its session is still live
CI / check (push) Successful in 48s
CI / deploy (push) Successful in 29s
Opening a session that's still recording (active in its summary) now
re-fetches every 30s and updates the map in place, instead of freezing
at whatever the track looked like when first opened. Stops on its own
once the session ends, or immediately if switched away from.
2026-09-06 23:42:34 +02:00
randy 6b8ea27e22 Show Live/Ended status on LiveTrack sessions
CI / check (push) Successful in 57s
CI / deploy (push) Successful in 26s
A session's own end timestamp is absent until the activity is
actually stopped, so its presence/absence is the status signal — no
new data needed, just reading what was already there. Shown as a
badge next to the session in the list and in its map hover popup.
2026-09-06 23:32:58 +02:00
randy 55607aece9 Show session summary on hover over its LiveTrack marker
CI / check (push) Successful in 55s
CI / deploy (push) Successful in 23s
The track endpoint now merges the session summary into the marker's
GeoJSON properties (already computed for storage, just wasn't passed
through), and the marker gets the same hover-popup treatment GPX
points already have.
2026-09-06 22:32:44 +02:00
randy 0ee98d8f07 Summarize LiveTrack sessions and prune stale ones
CI / check (push) Successful in 48s
CI / deploy (push) Successful in 23s
Fetching a session's track now also grabs its summary (name, activity
type, distance, duration) from the same page and stores it — sessions
the panel has actually shown replace their raw UUID with that summary,
and are kept indefinitely regardless of age. A session nobody ever
opened (no summary) is more likely test/dead data than something worth
keeping, so it's dropped automatically after 48h.
2026-09-06 22:18:25 +02:00
randy f4b5237f01 Render a session's track on the map
CI / check (push) Successful in 47s
CI / deploy (push) Successful in 19s
Clicking a LiveTrack session now fetches and draws its track (line +
latest position). Garmin's own API 403s any plain server-side request
(Cloudflare bot protection), so LiveTrackClient fetches the session
page instead and pulls the track out of the server-rendered data
embedded in it. The webhook now captures the session's token too,
since the id alone isn't enough to fetch anything.
2026-09-06 22:07:13 +02:00
randy 7083cec36c Cache-bust app JS/CSS on each deploy
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 18s
Browsers (and a caching proxy in front of production) were holding
onto /js and /css for hours, so a deploy could go unnoticed by an
already-open tab. A boot-time version query string on the entry
script/stylesheet, plus an import map remapping map.js's internal
module imports to the same versioned URLs, fixes that without editing
every JS file's import statements.
2026-09-06 21:41:57 +02:00
randy 566e80e38d Trigger CI: verify cache restore on second run
CI / check (push) Successful in 39s
CI / deploy (push) Successful in 7s
2026-09-06 21:29:54 +02:00
randy 7e984f9076 Trigger CI: verify actions cache fix on the runner
CI / check (push) Successful in 2m26s
CI / deploy (push) Successful in 12s
2026-09-06 21:26:30 +02:00
randy f9ddf860ea Add Garmin LiveTrack session tracking
CI / check (push) Successful in 6m49s
CI / deploy (push) Successful in 23s
A Gmail-watching Apps Script (documented in docs/deployment.adoc) posts
the session ID from each LiveTrack email to a new webhook, which stores
it in data/livetrack.json; a new sidebar panel lists what's been
received.
2026-09-06 21:05:35 +02:00
randy 60170efda2 Rename deploy runner label to cube-host
CI / check (push) Successful in 6m26s
CI / deploy (push) Successful in 22s
2026-08-28 20:38:16 +02:00
randy 3fbd97a0bd Add a weekly Renovate workflow for dependency updates
CI / check (push) Successful in 7m25s
CI / deploy (push) Successful in 14s
Runs renovate/renovate against this Gitea instance on the native
deploy runner, opening PRs for outdated Bundler dependencies instead
of relying on manually noticing (as happened with puma/mustermann
earlier).
2026-08-27 19:31:24 +02:00
randy 260006ad65 Support multiple allowed users for write access
MAPTOOL_ALLOWED_GITEA_USER now accepts a comma-separated list of
Gitea usernames instead of just one, for when more than one person
needs upload/delete/move access.
2026-08-27 18:25:31 +02:00
randy e13d1a1e97 Require Gitea login to write GPX data; IPv6-capable bind
CI / check (push) Successful in 6m28s
CI / deploy (push) Successful in 13s
Upload/delete/move/create-folder now require logging in via Gitea
OAuth2 (only MAPTOOL_ALLOWED_GITEA_USER is granted write access);
reading stays public. Deploy secrets (session secret, OAuth2 client
credentials, allowed user) are written to .env from Gitea Actions
repo secrets on every deploy instead of living on the server by hand.

Also switch the container's bind address to "[::]" (dual-stack) so
the app is reachable over IPv6, not just IPv4.
2026-08-27 18:02:37 +02:00
randy ae67cc72c7 Move deploy job to a dedicated native runner
CI / check (push) Successful in 5m43s
CI / deploy (push) Successful in 2m46s
The Docker-based runner's own container is too minimal for host-mode
jobs (no rsync, docker CLI, or Node.js) and has no reliable path to
the host filesystem or systemctl otherwise. Register a separate,
native runner (label deploy:host) just for deployment, and point the
deploy job at it.
2026-08-27 16:12:33 +02:00
randy 71e638b459 Fix CI: host-mode runner has no Node.js for actions/checkout
CI / check (push) Successful in 7m3s
CI / deploy (push) Failing after 1s
Host-mode jobs run directly in the runner container instead of a
spawned per-job container, so JS actions like actions/checkout@v4
have no Node.js runtime to execute with. Replace it with a plain git
fetch/checkout in the deploy job.
2026-08-27 15:39:47 +02:00
randy 3a79c3d6c1 Add Docker deployment via Gitea Actions host-mode runner
CI / check (push) Successful in 5m47s
CI / deploy (push) Failing after 1s
Containerize the app (Dockerfile, docker-compose.yaml) and add a CI
deploy job that syncs the checkout to /srv/docker/maptool and runs
docker compose directly, using the runner's new "host" execution
label to reach the host's Docker daemon and filesystem.
2026-08-27 14:54:21 +02:00
randy f186a3e8ef Fix CI: ruby/setup-ruby doesn't recognize "file" as a version keyword
CI / check (push) Successful in 5m47s
Omitting ruby-version entirely triggers its actual auto-detection from
.ruby-version — "file" was a mistaken guess and failed with "Unknown
engine file".
2026-08-27 14:06:31 +02:00
randy 7f2ad57d1a Add a Gitea Actions CI workflow
CI / check (push) Failing after 1m30s
Ruby syntax check, a require_relative "app" smoke load, and JS syntax
check on every push/PR. No test suite exists yet, so these are cheap
syntax/load checks rather than behavioral tests.
2026-08-27 14:03:37 +02:00
randy 0887ccbfe7 Drop the OsmAnd-recolor map theme, add offline labels and a lighter palette nudge
Remove osmand-style.js (the per-layer color-patching approach) and its
docs entirely — back to the plain OpenFreeMap "liberty" style online,
and protomaps-themes-base's stock theme offline as the base.

Offline maps previously had no text labels at all: the
protomaps-themes-base layers() call never passed a lang option, which
is required to include its label layers. Fixed, backed by vendored
Noto Sans Regular/Italic/Medium glyph PBFs (ranges 0-255 and 256-511
only — Basic Latin + Latin-1 Supplement + Latin Extended-A, enough for
accented Central European place names; other scripts won't render
offline). Verified against the actual offline.pmtiles vector layer
schema that the name/name:en/script fields this expects are present.

Also nudge a handful of OFFLINE_THEME color roles (background, water,
wood, buildings, the two road-importance tiers) towards the online
style's actual palette (sampled from its style JSON) so switching
Online/Offline doesn't look like two different apps — deliberately
partial, not the removed full recolor.
2026-08-25 22:38:56 +02:00
randy 06ea8ea141 Add file-kind icons to the GPX tree, fix drop into an empty panel
Each tree entry now shows a small waypoint/track icon based on a
cheap substring check for <wpt>/<trk> (Maptool::Gpx.kinds) rather than
a full REXML parse, since re-parsing a several-thousand-waypoint file
on every tree fetch would make the panel noticeably slower to open.

Also fix a real drop-target bug: .gpx-tree had no flex:1, so a
short/empty tree left blank panel space below it that wasn't part of
the drag & drop target — dropping a file there silently did nothing.
2026-08-25 22:38:41 +02:00
randy 87df90f75c Redesign UI as an OsmAnd-style sidebar, add live GNSS position via gpsd
Restructure the map controls from MapLibre corner buttons into a left
sidebar of icons with flyouts/panels (radar, GPX, GNSS), matching
OsmAnd's layout more closely:

- Base map and GPX selection are now full-height side panels docked
  next to the sidebar (not small dropdown flyouts or a right-docked
  panel), opening/closing exclusively via a small shared event bus
  (panels.js) so only one is ever open at a time.
- The GPX panel's upload/new-folder controls are now icon buttons in
  the header instead of an always-visible dropzone and form; dragging
  a GPX file onto the tree, a specific folder, or straight onto the
  map itself all upload it.
- New: Maptool::GpsdClient (lib/maptool/gpsd_client.rb) streams live
  GNSS fixes from a local gpsd daemon over Server-Sent Events
  (/api/gnss/stream) to a marker on the map (public/js/gnss.js) — one
  background gpsd connection shared by every browser tab.
- The online map style (public/js/osmand-style.js) is now styled to
  resemble OsmAnd's own rendering, applied to both the online and
  offline (PMTiles) styles.

Also adds a GeolocateControl (browser geolocation) next to the zoom
controls.
2026-08-25 19:39:13 +02:00
randy 008e034932 Add geolocate control to the map
Lets the user center the map on their current location, with an
accuracy circle and continuous tracking.
2026-08-25 15:18:55 +02:00