Note: this only reflects my personal work this month. Visit the BTC Map Blog for consolidated monthly reports.
Table of Contents
Thanks
Thanks to Nathanael for testing the new geofences and admin interface in Switzerland.
API
The API absorbed the bulk of my time again this month. The big theme was access control, since we need to decentralize admin work and let locals maintain their own data. A new area_manager role and a per-user geofence that constrains both area and event managers to a region they are actually responsible for.
Geofences
I added a geofence column to the user table that holds a comma-separated list of area ids. When non-empty, its behavior is role-dependent:
event_managercallers are rejected if the event’sarea_idor(lat, lon)falls outside every fenced area’s GeoJSON.area_managercallers can only mutate areas with fullidmatch regardless of GeoJSON, so they can’t touch nested areas, andadd_areacall is rejected outright (admins and roots still create areas on request).adminandrootare exempt, and an empty geofence means unrestricted.
A new set_user_geofence RPC sets or clears the whitelist.
Area Manager
The new Role::AreaManager unlocks add_area, get_area, set_area_tag, set_area_image, remove_area_tag, remove_area and search.
Place Reports
I added a new fire-and-forget report_place RPC that lets trusted external sources record typed reports (verified, refused_sats, out_of_business, …) against an existing BTC Map place. It comes with a new place_report table (place_id, origin_id, extra_fields JSON, ticket_url (populated by a future background job, not user-facing), and the usual soft-delete columns).
I also added the public GET /v4/place-submissions endpoint, which exposes open, non-revoked rows from place_submission with an optional source= filter.
RPC Refactor
Most area RPCs were moved from the flat src/rpc/ into dedicated src/rpc/area/ and src/rpc/event/ submodules. It is mostly churn, but it makes the directory tree match the docs tree.
I also renamed generate_bboxes to generate_area_bboxes for consistency, and generate_element_icons now iterates every element in the database via select_updated_since(UNIX_EPOCH, ...) instead of looping over an id range.
Docs
The area RPC docs were split from a single area-methods.md into per-method files under docs/rpc/area/, mirroring the structure already used by docs/rpc/event/. Each file follows the event pattern: Description, Params, Result Format, Allowed Roles, and Examples. I also added an event RPC API index page and refreshed the main index pages.
Auth & Logging
Handlers (and the rest::auth Auth extractor) now write the resolved user id into the request extensions via AuthenticatedUser. The Log middleware reads it back so the request table records who is calling each endpoint instead of always leaving user_id NULL.
I also replaced the raw Query returned no rows error that surfaced when calling an RPC with an unknown or revoked bearer token. It’s now a friendly Invalid bearer token JSON-RPC error, with focused tests for both cases.
Electrum
A new ping_electrum_servers RPC probes every configured electrum server in parallel with the standard server.ping JSON-RPC request and reports per-server success/failure.
Each server has a 5-second timeout. The outer call never fails because of a single unhealthy server — the results just carry ok + latency_ms or ok=false + error. Allowed for admin and root roles.
Area BBox Sync
add_area and set_area_tag (when applied with geo_json) used to leave the bbox_* columns stale until someone manually ran generate_bboxes. Both paths now derive the bbox from the stored GeoJSON via the existing BboxGenerator and update the columns in the same transaction. set_area_tag also validates geo_json before persisting it.
Maintenance
- Pinned the build target CPU
- Fixed a pile of clippy lints
- Counted only active
area_elementsingenerate_reports(it used to include soft-deleted ones) - Filtered soft-deleted
area_elementsfrom area-scoped feeds innew_places_for_areaandget_comments, with a regression test - Fixed
started_atbeing overwritten in thegenerate_reportsresponse - Cleaned up several README files
CLI
The CLI picked up wrappers for most of the new API surface:
place reportcalls the newreport_placeRPC (the oldimportmodule was renamed toplaceto match)electrum-server pingwrapsping_electrum_serverswith an--include-deletedflag matchingget_electrum_serversarea addwrapsadd_areaarea generate-bboxeswraps the renamedgenerate_area_bboxeselement generate-iconswas renamed fromgenerate-element-iconsto match the new no-argument RPC signaturearea generate-element-mappingwas renamed fromgenerate-areas-elements-mapping
Dashboard
I hid bots and other non-user traffic from the unique IPs pie chart on the dashboard. They were swamping the slice for actual BTC Map users.
Event Admin
Event editing is now fully wired up. The marker popup got a Rename action that swaps the event name for an inline editor (Enter to save, Esc to cancel) and calls update_event via RPC, and an Edit button that opens the full event form.
I also fixed a shadowed event variable that was preventing update_event from firing, and surfaced the real error message from the API’s error.data envelope (e.g. Location (51.5, -0.1) is outside your geofence) instead of the generic Server error.
Android
Most of my Android work this month was around build infrastructure rather than features.
Build Infrastructure
- Bumped AGP to 9.3.2 and Gradle wrapper to 9.5.0
- Added release signing config (previously the signing key lived in CI; it now lives in
app/build.gradle.ktsand the CI workflow drops its signing block) - Added a new
betabuild type, with a matchingapp deploycommand in the devtools script
Dev Tools
I added a devtools helper script at the repo root that wraps the common emulator and app commands: emulator start/stop, app install/run/uninstall/deploy. The script is documented in AGENTS.md.
OpenStreetMap Attribution
Added a small “© OpenStreetMap contributors” attribution overlay to the map fragment, with a settings toggle to show or hide it.
Bug Fixes & Polish
- Fixed a
ConcurrentModificationExceptioninViewportCacheby snapshotting items before serializing GeoJson whenonCameraIdlefires while a previousloadInBoundscoroutine is still iterating - Updated the Material Symbols font to the 2026-08-28 snapshot, switched glyphs to the outlined variant, and added a handful of new icons (
18_up_rating,destruction,dresser,favorite,info,water_drop)
Ansible
I moved the API backups to a new dedicated volume.
Website
I added Skate Spots as a new BTC Map app sponsor. Nothing else.
Blog
I migrated one of my old historical BTC Map posts from this site into the official BTC Map Blog, so all of our consolidated reporting now lives in one place.
Future Plans
A few things on my mind:
- Cut a new Android release.
- Keep working on RPC documentation.
- Continue the RPC submodule migration.
- Add a dashboard widget for the place-report data once we have enough volume to make it interesting.