AI Web Launchpad

Ben's Mobile Vibe

A small home for apps built and run by Claude Code. Pick one below.

Under the hood it's one lightweight reverse proxy, routing HTTPS traffic to whichever small apps are running behind it — the interesting part isn't the proxy itself, it's what it enables: a standing framework for spinning up a new project, wiring it in, and having it live at its own address within the hour, built entirely from a phone or tablet on the road. No laptop, no per-app hosting setup — just point it at a port and go.

What Trip Log does

Trip Log turns a phone into a proper travel diary: an interactive map tracks every stop, live GPS tracking follows a trip in real time while it's happening, and photo and video pins land automatically at the spot where they were captured — no manual tagging. Multiple trips stay organized side by side, each with its own map and timeline to browse back through later.

A native Android companion app extends that further: in-app camera capture that keeps full location data (something the phone's regular photo picker strips out), background tracking that keeps recording even if the app is closed or the phone reboots, and bulk import from an existing photo gallery so a trip already underway can be brought in all at once.

from the build log

The little things: building Trip Log with an AI pair and an Android tablet

Trip Log is the second app on the Mobile Vibe stack — a self-hosted trip tracker: a map, a passphrase-gated write side, live GPS tracking, photo/video pins, and a native Android companion app. None of that is the interesting part. The interesting part is everything that broke along the way, because almost none of it broke where the docs said it would.

Split-screen tablet view: Claude Code running in a terminal on the left, the live Trip Log map with a real Vietnam route and cluster markers on the right
The actual dev environment for this entire project: a split-screen tablet, one side running Claude Code over SSH, the other watching the live app update in real time.

Here's the constraint that shapes everything below: this whole project was built on a Google Compute Engine VM running Claude Code, with the only human interface being an Android tablet — Termius as the SSH client, nothing else. No laptop, no desktop, at any point. Every file transfer, every real photo, every real device test, every wireless-debugging pairing session — all of it happened through a tablet talking to a cloud box. That's not a gimmick detail; it's the reason the discipline below (get the real artifact, test against the real device) wasn't optional. There was no local dev environment to fall back on, no “let me just try this on my machine real quick.” The tablet was the machine, and the VM was the only place code actually ran.

Twenty-eight sessions in, the pattern is consistent: the framework-level plan is usually right, and the actual bugs live one layer down, in the gap between what a spec says and what a real device does. Here's the running list.

Python 3.13 quietly deleted a stdlib module

The first session needed multipart form parsing for photo uploads. The obvious move is cgi.FieldStorage — except Python 3.13 removed the cgi module outright (deprecated since 3.11, gone for real now). First real obstacle, session one, before a single feature existed: hand-roll a multipart parser against the stdlib's raw socket/HTTP primitives instead. Small thing, but it set the tone — check the actual runtime before trusting a memorized API.

The real Google Takeout file didn't match anyone's docs

Session 3 needed to import a Google Maps Timeline export. Every description of the format — including the ones baked into earlier planning — assumed clean, numeric {lat, lon} pairs. The real on-device export, pulled via the tablet and SFTP'd over, told a different story: three different segment shapes mixed together (timelinePath, visit, activity), coordinates as literal strings like "42.61°, -71.41°", and timestamps carrying real UTC offsets that needed normalizing before they were usable. The parser got rewritten against the real file, not the assumed one, and CSV support — which the design docs also called for — got explicitly deferred because no real CSV sample existed to build against. That became a standing rule for the rest of the project: nothing gets built against assumed structure when a real sample can be obtained first.

EXIF GPS parsing: byte-exact, and still wrong the first time

Photo geolocation (session 4) meant walking raw JPEG bytes by hand — APP1/Exif markers, a TIFF header, IFD0's GPS-IFD pointer, DMS RATIONAL triples. First pass worked against 7 real photos from the tablet. It broke the moment a second device entered the picture: an iPhone photo, tested a session later, turned out to be big-endian TIFF where the tablet's photos were little-endian. The byte-walker handled both without changes once the assumption “assume the tablet's endianness” was replaced with “read the header's own endianness flag” — but that gap only surfaced because a second real device got tested, not because anyone predicted it.

The Android GPS-stripping saga

This is the one that ate the most sessions, and the one worth telling in full, because the fix kept looking finished and then wasn't.

The finding: Android redacts GPS EXIF from any photo handed to a web app through the system file/photo picker, unless the app holds the special ACCESS_MEDIA_LOCATION permission. No browser API exists to request that permission — so this isn't a Trip Log bug, it's every web app on Android, full stop.

First attempted fix: capture="environment" on the file input, to force the camera instead of the picker. Confirmed on a real device: it doesn't reliably work. The browser/OS still landed on a chooser.

Second attempted fix: assume it's the picker UI that's the problem, and try a plain generic file browser instead of a photos/camera-specific dialog. Also confirmed not to help — the redaction happens at the content-resolver level, independent of which picker UI presents the file. Two attempts, two real-device tests, two dead ends, and a clean negative result recorded either way: no in-browser file-selection path sidesteps this. That's not wasted effort — knowing definitively what doesn't work is what let session 13 go straight to a real fix instead of trying a third picker variant.

The actual fix: stop reading files through the picker at all. Session 13 built an in-page live camera with getUserMedia and MediaRecorder — shutter button draws a video frame to an offscreen canvas, video capture streams through MediaRecorder, and location comes from navigator.geolocation at the moment of capture, not from EXIF at all. A canvas frame has no EXIF to redact. Confirmed working on the real tablet: both a live photo and a live video landed as correctly-placed pins.

Where it stands: in-the-moment capture is solved. Gallery-picked photos were still affected at the time — that's what led to session 14's native Android app, where ACCESS_MEDIA_LOCATION plus MediaStore.setRequireOriginal() pulls the original, unredacted bytes directly off the device and uploads them through the exact same /api/entries endpoint the browser path already used. Same backend, zero server changes — the fix was entirely about which client reads the file.

Building a native Android app needed a second VM

The dev VM had no Java, no Gradle, no Android SDK, and not enough RAM or disk to get any of them running comfortably. Rather than cramming a full Android toolchain onto a box that wasn't sized for it, session 14 spun up a second, purpose-built GCE VM just for compiling — powered off between builds to save cost, its disk (JDK, SDK, Gradle caches) persisting so nothing reinstalls per build. The dev VM's own boot disk still needed a live resize (10GB → 20GB, no downtime) just for headroom. No git remote exists between the two, so the Android source and the built APK move by gcloud compute scp.

The toolchain specifics weren't guessed in advance, either — they were pinned by what actually built: JDK 21 (Debian 13 doesn't package 17), Android Gradle Plugin 9.2.1. One genuinely surprising discovery, found via a failed first build rather than anticipated: AGP 9.0+ builds Kotlin support in — no separate Kotlin Gradle plugin, no kotlinOptions{} block. Most Android/Kotlin guides written before 2026 still describe the old two-plugin setup, and following them verbatim produces a build error, not a working app.

androidx.security is dead, so the token store got hand-built

Google deprecated androidx.security:security-crypto in 2025 with no stable replacement. The login flow needed to persist a session token without stashing it in plaintext SharedPreferences, so SecureTokenStore.kt wraps Android Keystore-backed AES/GCM directly — one more case where the textbook answer had quietly expired underneath the project.

A native menu got reverted the same day it shipped

Not every fix in this project was a bug — some were second-guessing a decision fast enough that it never cost anything. A native ActionBar (camera icon, login/logout, an overflow menu for the import flows) went in, got built, got installed — and came back out a few hours later, on direct real-device feedback. The web buttons already did everything the native ones would have: the browser bridge inside the WebView gave them GPS-preserving gallery access and camera/mic/location permissions just by running inside this app, no native duplicate required. That reset a rule for the rest of the project: native Android surface area only for what a browser genuinely can't do, so the web layer keeps iterating without ever forcing a rebuild.

The same session turned up something worse, unrelated to the menu at all: every native photo upload since session 14 had been silently broken. TripLogApi.kt sent uploads with chunked Transfer-Encoding and no Content-Length header; server.py's body reader only knows how to read a Content-Length. The entry still got created — a real row, a real id — with no photo and no GPS, and nothing about a 200 response said otherwise. It surfaced as 17 real “ghost” entries sitting in the live trip, found by looking, not by a failing test. Soft-deleted after confirming with the user; the fix was one line (stop asking for chunked mode; the default buffering computes Content-Length correctly on its own).

The app's own title bar was invisible, and the DOM disagreed

Real-device testing after a run of Android sessions turned up a bug that shouldn't have been possible: the web page's top bar — trip picker, “☰ Menu,” the login chip — was completely unreachable in the installed app. Not broken-looking. Gone. Except the DOM said otherwise: getBoundingClientRect() reported correct bounds, hit-testing found the right element, everything about the page insisted it was sitting right there, visible and tappable.

Real device screenshot showing only the bare Trip Log title strip, with the rest of the page blank
The bug, on the real device: nothing below the native “Trip Log” title strip — the whole web UI was there, just painted over.

The resolution came from treating “the DOM says it's fine” as data, not an answer — adb forward onto the WebView's own DevTools socket, and a hand-rolled WebSocket client speaking Chrome DevTools Protocol directly (no browser was available in this environment to just open chrome://inspect). That confirmed the page was exactly where it said it was — a separate, higher native layer was simply painting over it. The real cause: this device enforces edge-to-edge layout by default, and the Activity never consumed its own window insets, so the WebView drew full-bleed underneath the native title strip instead of below it.

Two native-side attempts at a fix each independently caused the WebView's renderer to hang completely — one padding the WebView from an insets listener, the other reading the theme's action-bar size from a separate DecorView listener and only ever handing the number to the page via evaluateJavascript(). Neither touched WebView layout directly on the second attempt, and it hung anyway. Both were reverted immediately rather than ship a hang, and why either one coincided with the hang was never pinned down.

Split-screen tablet view showing the diagnostic code comment being written into MainActivity.kt live, next to the running app
The real fix, written live against the running app — and the comment recording exactly why, for whoever reads this next.

The fix that actually shipped needed no native code at all: the page detects window.AndroidApp itself and sets a CSS custom property, --native-top-inset, that the top bar and its dropdown already consume, defaulting to 0px everywhere else. The exact pixel value came from live DevTools CSS injection against the running app, not a guess — 56px still clipped part of the bar, 70px looked clear until a later real-device check found it still slightly covered, 84px cleared it with headroom.

A one-line permission gap, found by logcat, not by guessing

The top bar being reachable again surfaced the next bug immediately: tapping “Start tracking” crashed the app outright. logcat named the exact line — a background-connectivity callback added for persistent tracking called an API that throws a SecurityException without ACCESS_NETWORK_STATE, a normal manifest permission nobody had added. One line fixed it. The interesting part isn't the bug, which was trivial once found — it's that “trivial once found” only happened because the fix came from reading the actual crash log on the actual device, not from re-reading the code and guessing.

Real Android runtime permission dialog: Allow Trip Log to access this device's location, with Precise/Approximate and While using the app / Only this time / Don't allow options
The real permission dialog behind all of this — background location tracking only works if a real device grants it, and a real device is the only place that's provable.
Claude Code on the tablet, mid-fix: the crash log, the missing ACCESS_NETWORK_STATE permission line, and the diff adding it, all in one session
The other half of it — the crash log, the missing manifest permission, and the fix, all in the same Claude Code session on the tablet.

An outage test cut the connection doing the testing

Verifying that tracking survives a real connectivity outage meant staging one — disabling WiFi and mobile data on the tablet via adb for a stretch, to confirm queued points survive and flush cleanly once the connection returns. They did: 24 points queued locally with zero loss or reordering across a real 15-plus-minute gap. What the test plan hadn't accounted for: the wireless-adb connection driving that same tablet rides the same network being switched off. The outage cut the tunnel controlling the test along with the tunnel being tested, and the tablet had to be manually reconnected once WiFi came back. Real finding, filed away for next time: don't toggle connectivity on a device you're controlling over that connectivity.

Claude Code running inside Termius on the tablet, mid-test, diagnosing the T-118 background-location test
The whole test marathon, driven from the tablet itself — there was never a second machine to fall back on.

A live import landed on the wrong trip

Testing session 26's bulk video import against real gallery content found a bug that had actually been live since session 14: the native import path never read which trip was selected in the web UI at all — it always uploaded to the server's separate notion of the “active” trip, silently. A real test import proved it the hard way, landing 20 entries on the live, public “Vietnam 2026” trip instead of the private test trip that was visibly selected on screen at the time. Cleaned up immediately — all 20 soft-deleted, the real trip's entry count and newest id confirmed back to exactly where they'd been — and fixed by having the native side read the web <select>'s current value before uploading, the same value the browser upload path had been reading all along.

Gallery bulk-import picker mid-selection, showing a mixed grid of 188 available photos and videos
The bulk-import picker that exposed the bug — 188 real photos and videos from an actual trip, not a synthetic fixture.

Diagnosing a choppy video without guessing a third fix

A user report — a specific video, recorded at 12:43:50 PM local time, played back choppily in the installed app — turned into a small lesson in knowing when to stop patching and go measure. First fix: the media server always sent a full file with one 200 and no Accept-Ranges, which a mobile WebView needs for a big video to seek or sometimes even play at all. Added real HTTP Range support. Didn't fully fix it. Second fix: the server was still defaulting to HTTP/1.0, closing the TCP connection after every response, which meant every one of a video's many sequential range requests paid for a fresh connection — expensive over mobile RTT. Added HTTP/1.1 keep-alive. Still choppy.

Rather than guess a third server-side change, the next step was to go look: a real headless Chromium (Playwright, already on this VM) drove the actual live page, logged in through the real form, and reproduced the exact video element the app's own click handler creates, then recorded 20 seconds of real playback against real network timing. The finding: every stall had 10 to 30 seconds already buffered ahead of the playhead. Not network-starved — decode-bound. A small hand-written MP4 box parser (no ffprobe available, and this project doesn't reach for external tools anyway) confirmed the file was 1080p H.264 High Profile Level 4.0 at roughly 20 Mbps, close to the ceiling that level supports. The user confirmed the same file played back smoothly through the phone's own native player over a regular mobile browser download — a WebView decode limitation, not a server problem, and not one more server-side fix to chase. The one real fix that came out of the whole investigation was a download button, so a video that a WebView can't decode smoothly can still be handed to a player that can.

Bundled into the same rebuild, at the user's explicit request: a rotation bug the user specifically remembered as needing a native rebuild. Worth double-checking memory against the record before trusting either one blindly — a quick pass through git log and this project's own docs could easily have suggested the wrong thing, since an earlier, unrelated fix already covered a web-only pan/zoom quirk on rotation. The user's memory turned out to be right: the real fix (android:configChanges, so the Activity resizes in place instead of being destroyed and recreated) had been scoped once before but never actually written. Shipped with the known, flagged risk left visible rather than hidden: the same category of WebView-resize operation had correlated with a renderer hang twice already in this project, and that history sat in front of the user before deciding to ship it anyway.

The launchpad had never seen a POST

Trip Log's write routes (login, tracking, entries) sit behind a shared reverse-proxy launchpad that fronts every app on the box — the same one serving the page you're reading this on. That launchpad had, up to this point, only ever served read-only apps — its do_POST handler didn't exist. Invisible until the first app that actually needed to write data showed up. Fixed on the launchpad side (a separate, frozen repo this project doesn't touch directly), but worth noting as a class of bug: infrastructure that's “worked fine” for years because nothing before you actually exercised the path.

The launchpad also wedged once in production — stopped accepting connections on :443 entirely while the app behind it kept answering fine on its own port, diagnosed via a backed-up listen queue and a 19-minute gap in its own logs. systemctl restart fixed it in seconds; the actual root cause (single-threaded HTTPServer blocking on a hung client, unconfirmed at the time) got its own dedicated root-cause pass in a later webplatform session — the fix moved the TLS handshake off the shared accept loop and into each connection's own worker thread, so one bad handshake can no longer wedge every other visitor's connection, including this one.

The proxy was buffering everything in memory

A report that media was loading painfully slowly — thumbnails and videos specifically, while map tiles and the small JSON behind pins loaded normally — landed the day after the video quality bump made every file bigger, and that wasn't a coincidence. I measured instead of guessing: the same 66MB file took 0.2 seconds served straight from Trip Log's own backend, and 1.1 seconds through the shared reverse-proxy that actually fronts it in production — a real, measured 5.5x overhead. Reading that proxy's own code — read-only, nothing edited, that repo stays off-limits from this one — turned up the cause: it reads an entire proxied response into memory before writing a single byte back to the client, for every app it fronts, not just this one. Three concurrent large downloads pushed its own memory from about 176MB to 255MB in real time, consistent with memory spikes already visible in its own systemd accounting over a six-day uptime.

The fix is small and already written up — swap that one full read for a chunked read/write loop, so memory use per request stays flat instead of scaling with file size — but it lives in the same off-limits repo as the do_POST bug and the earlier TLS wedge, so it gets handed off rather than patched from here. Restarting the service was enough to get memory back down and everyone reachable again in the meantime.

A same-second ordering bug, caught live

Small one, but a good example of what “smoke test against something real” catches that a synthetic test doesn't: the trip picker's “most recent trip first” ordering used started_at DESC alone. Two trips created in the same second sorted non-deterministically. Caught during a live smoke test, fixed by adding id DESC as an explicit tiebreaker — the kind of bug that a hand-written test fixture would have had to go out of its way to reproduce, but that real usage found in minutes.

Wireless debugging kept killing itself mid-pairing

The very last piece — actually installing the Android app on the real tablet — turned into its own small saga, and it started before adb even entered the picture: the GCE VM has no direct network path to a tablet sitting on a home WiFi network, so step one was standing up a WireGuard tunnel between the two just to give each side an IP the other could reach at all.

With the tunnel up, the next problem was purely human-interface: Android's wireless-debugging pairing screen gets backgrounded (and its one-time pairing code invalidated) the moment you switch apps to go read the code off it — which is exactly what pairing normally requires you to do. The fix wasn't a code change to Trip Log at all: a tiny, dependency-free Python web form, bound only to the WireGuard tunnel IP, serving a two-step form that shell-runs adb pair and then adb connect.

That form still needed to be opened from a second device on the tunnel, though — and the only other device around was his wife's phone. She loaded the form and typed in the six-digit code read off the tablet, so the pairing went through without a single app-switch on the tablet itself. Small, slightly absurd detail, but a real one: the last mile of getting a native Android app onto the one tablet this whole project runs on required borrowing a second phone. Also worth remembering for next time: the pairing screen's port and the main “connect” port are different and single-use — reusing the pairing port for adb connect fails with a misleading “connection refused” that looks like a tunnel problem and isn't.

Google strips GPS from shared albums, on purpose

Not a bug, but a real finding worth recording: a shared Google Photos album was investigated as a possible “let anyone drop photos in without a Trip Log account” surface. Tested against five real photos from an actual shared album, including a Canon EOS R6 shot alongside tablet photos — zero of five carried GPS, even though other EXIF (camera model, date, orientation) survived intact, and even at full original resolution via the =d download variant. Google strips GPS specifically, as a deliberate privacy default, from anything served through a public share link. Confirmed with real data rather than assumed from a privacy policy — and it changed the shape of a whole feature idea before any code got written for it.

A public link for photos, and a tap for the ones with no GPS

Every other write in this app sits behind a login — the passphrase-gated half of the public-reads/passphrase-writes split this project has stuck to from day one. But not everyone who might have a photo worth adding is a Trip Log user, and the only realistic way to get a photo off someone else's phone and into the app is a link they can just tap. So GET /upload exists as this app's one deliberate exception: a public write endpoint, no login required. I had Claude flag it explicitly against the project's own public-reads/passphrase-writes rule before it got built, rather than letting it slide in as an ordinary feature — a public write surface needs guardrails an authenticated one doesn't, so anything that lands through it gets tagged source='unsorted', a hard 200MB size cap checked before the body is even read, and an extension allowlist none of the other upload paths bother with.

The other side of the same problem: what happens when a photo — from that link, from a gallery import, from anywhere — has no location data at all. I didn't want that guessed at or defaulted to something. If I'm logged in and looking at an entry with no pin, I can drop one myself: “📍 Pick location on map,” a literal tap on the live map.

The real Trip Log menu: Pick location on map, Select from gallery, and the Dashcam Gallery link, over a live map of the actual Vietnam trip
The real menu on a real trip — “Pick location on map” for anything missing GPS, and the new Dashcam Gallery link, both live at once.

124 dashcam clips with no GPS at all

The next real batch of footage wasn't phone photos — it was dashcam video, front and rear channels, roughly 124 clips, and none of it carrying any location data whatsoever. I had that confirmed by hand, by parsing the AVI/RIFF container directly rather than trusting an assumption about the format. Getting it into the app meant solving two separate problems: compressing about 35GB of raw footage down to something the site could actually serve, and figuring out where each clip had actually been shot.

Compression pulled ffmpeg into this project — only the second dependency this whole stdlib-only build has ever taken on, after the Leaflet/OSM map tiles, and I signed off on installing it explicitly rather than it just quietly showing up. Location came from something simpler than GPS: matching each clip's own filename timestamp against the real GPS trail the phone had already logged that day. 96 of 96 real clips matched, most within a few seconds of the nearest logged point, worst case under two minutes off. The gallery doesn't hide that uncertainty either — it says “close match” or however many minutes off right on the clip, instead of pretending every match is exact.

The Dashcam Gallery: real clips from the Vietnam trip, each with a timestamp, matched coordinates, and a close-match label
The dashcam gallery, matched against the real trail — timestamp, coordinates, and how close the match actually is, shown plainly on every clip.

One aside worth keeping from partway through that batch: I stopped a full re-verification pass midway through and told Claude to just check one file's sha256 instead, since the source footage hadn't changed and re-hashing all of it was burning real cycles for no reason. Small thing, but a good instinct — match how much verification a step actually needs, not the maximum available.

The excuse that stopped being true

Choppy video playback got a real diagnosis back in session 28 — a genuine WebView decode limit, confirmed with a headless browser and a hand-written MP4 parser, not a server bug at all — and shipped with a download-button workaround instead of a real fix, because at the time there was no video encoder anywhere in this project to build a real fix with. That was a reasonable place to stop. It just didn't stay true.

ffmpeg landed in this repo less than a week later, for a completely unrelated reason: compressing dashcam footage. Nobody connected the two until I said so directly — still hitting the same choppy playback, and pointing out that other sites manage this fine, we should too. With ffmpeg already sitting there, transcoding every uploaded photo and video into a web-friendly rendition on ingest — scaled down, H.264, faststart — turned into a straightforward build, plus a retroactive pass over the entire existing backlog.

I didn't take the first settings that worked, either. I asked for real side-by-side comparison sets — regular trip video and dashcam video both — cut at a few different quality levels, and picked from actually watching them on my own tablet rather than trusting a CRF number on paper. The dashcam settings I picked ended up wider than the pipeline's first default (1280px instead of 854px for the front camera, full width for the rear, no artificial frame-rate cap) — the first pass at “good enough” undersold what the footage could actually look like.

A quality bump exposed a second bug

Bigger files uncovered a second bug the smaller ones had been quietly hiding: the transcoded output was missing -movflags +faststart, so each MP4's own index sat at the end of the file instead of the front. Harmless on a small clip. Genuinely painful once the quality bump made every file 2.5 to 6 times bigger, because the browser now had to seek all the way to the end of a much bigger file just to read its own metadata before it could even show a preview. I had it fixed without touching a single re-encode — a fast remux, just re-ordering the existing bytes, took about half a second per file and covered the whole backlog, 241 of 241. The bug itself had probably been there the whole time; the quality improvement is just what made it visible enough to notice.

The pattern underneath all of it. None of these were exotic bugs. They were all versions of the same thing: a spec, a guide, or a memorized default turning out to be stale, incomplete, or simply untrue the moment it met a real device, a real file, or a real second phone. The fix, every time, was the same discipline — get the real artifact (the real Timeline export, the real photo, the real tablet, a second real phone), test against it directly, and only build the narrow thing that real evidence actually supports. Nothing here was clever. It was just consistent.

And none of it had a shortcut. On a laptop, half of these would have been a five-second local check — plug in the phone, open dev tools, try it. Here, every one of them meant round-tripping through the VM: SFTP a real file over, run the parser, delete it after; install a debug APK on the actual tablet because there was no emulator to fall back to; build a whole second cloud VM just to get a Java toolchain that wasn't going to run on the primary box. The tablet-and-VM constraint didn't make the project harder in the way you'd expect — it just meant there was never a fake environment to be fooled by. Whatever worked, worked on the real thing, because the real thing was the only thing available.

The real Vietnam 2026 trip in Trip Log, showing live tracking controls, trip management options, and a download-full-backup link
Where it stands today — the real trip this whole project has been tracking, tracking itself.

This isn't a wrap-up post — Trip Log is still actively being built. The native Android app now handles auth, gallery import (photos and video), in-app live camera capture, and background location tracking that survives the app being killed or the device rebooting; a dashcam gallery matches GPS-less footage against the real trail and lets me add clips to the map on request; every photo and video gets a web-friendly transcoded rendition on ingest, faststart and all. A real in-app map view, editing or deleting a bad pin, and letting more than one person contribute to a trip are all still sketched but not built. Whatever the next round of little things turns out to be, it'll get added here rather than starting a new post.