Every improvement, feature, and fix — documented as it ships.
rows.life/notes introduces the Notes feature to people who haven't opened it yet: folders to any depth, images and PDFs inside notes, an editor built for very long notes, favorites, history and sorting, safe editing across devices, and the same zero-knowledge encryption as your rows. It also walks through moving in from Evernote or Obsidian with the command-line importers, including the merged import for people who used both. A new Notes item in the site header links to it from every page.
A row could lose its notes without anyone deleting them: a notes editor that goes blank on its own — an idle tab whose row re-renders while the cell has focus, a mobile editor rebuilt after the app is backgrounded — was read as an empty note and saved as a deletion, leaving the row's time and category intact but its text gone.
Now an empty editor only clears notes when you actually edited it. An editor that blanks by itself puts the text back and writes nothing. This applies on desktop, in the edit dialog, on mobile web and in the TUI — the previous attempt only covered the desktop sheet, which is why it never protected mobile.
On the server, a save that arrives with no notes for a row that has them now keeps the notes instead of erasing them. It no longer answers with an error: the earlier version refused those writes, which broke ordinary edits that only touched a time or a category, and — because the offline queue treats a rejection as permanent — retired the queued edit to the recovery list instead of saving it. Preserving is silent and safe, and older clients keep working unchanged.
If an edit went missing during that window, it may still be on your device: Settings has a recovery list of rows stranded locally.
On the Mileage report, hovering a month showed source readings that skipped a reading sitting inside that month. A month ending with a reading (e.g. Jul 31) displayed the next reading past the boundary (Aug 4) as its endpoint, even though August already used the Jul 31 reading as its start — so the same reading appeared on one side of the boundary but not the other. The tooltip now anchors on the last real reading inside the month. The distance totals were already correct and are unchanged; only the tooltip wording is fixed.
Pressing the insert-row shortcut could drop the new row two or three rows further down instead of directly under the row you were on. When the new row shared a start time with the rows around it (common, since each row starts where the previous one ends), it slid to the bottom of that group. New rows are now anchored directly beneath their source row. As part of the same fix, inserting a row into the middle of a past day no longer stamps it with an end time of "right now" — that used to create a phantom multi-hour row.
A row added on the phone could still be left below the bottom of the list, with a trip to another view and back the only way to bring it up. Scrolling to the new row was attempted for two frames after the editor closed, and on a phone that is not long enough: the software keyboard is still sliding away, the list is still being resized around it, and a scroll asked for mid-resize is quietly dropped. The app now keeps asking until the row is measurably on screen, and holds the row in the list while it does so, rather than betting on catching the right frame. Starting to scroll yourself cancels it immediately.
Saving a new row on the phone left it out of sight below the bottom of the list — switching to another view and back was the only way to bring it up. The list works out how far it can scroll from a fixed height per row, which leaves out the header drawn at the start of each day, so on days with only a row or two the list is taller than it thinks and jumping to the "bottom" stopped short of the last rows. It now scrolls by its real height, and straight to the row you just saved. Rows waiting to sync are also rebuilt from the pending queue, so one added offline still appears after closing and reopening the app.
A row added offline is held on the device until it can be uploaded. If that upload was interrupted and the queued copy was lost, the row quietly stopped appearing once you were back online — the server had never heard of it, so there was nothing to show and nothing in the deleted list either, even though the row was still sitting on the device. Settings → Recover Rows now has a "Rows Saved On This Device" check that finds those rows and puts them back with their original date, time and notes. Rows still waiting to sync are left alone, and a row you deleted on another device is never brought back.
Adding a row could silently consume an existing one. To avoid stacking blank rows, + would hand back the last row instead of making a new one whenever that row had no category and no notes — but a real tracked row can look exactly like that (say 7:00–10:00, not categorised yet). On the phone, + then opened it as if it were new, and saving replaced the morning with the new entry: no delete, nothing in the recoverable list. Only a blank row created moments earlier is reused now. Two related fixes: a row still waiting to sync no longer disappears from the list when the device has no local copy of it, and edits that fail to upload are kept and retried instead of being discarded — previously an expired session during sync could throw away a row created offline before it ever reached the server.
Adding a row while offline worked, but going back to the list could show it missing — and sometimes the list came back empty. Three things were behind it. The list rebuilds itself from the local cache when there's no connection, and a row saved a second earlier could lose the race to be written there; it now keeps any row that's still waiting to sync. An empty local cache no longer blanks a list that's already on screen. And refreshing your login token — which needs the network — was being read as an expired session, which pushed the app toward a login screen it couldn't load and, worse, could discard rows still queued to sync. That's now recognised as "offline", not "logged out". Launching the app offline also works from a cold start: your encryption key is kept on the device, so it unlocks without the server.
The new rows import-health command streams scalar measurements directly from an Apple Health export ZIP and stores them as zero-duration rows.
Start with rows import-health ios-health-export.zip --preview to see the supported series, record counts, date ranges, and units found in the export. Select one or more series with --series, for example --series weight --series resting-heart-rate, then remove --preview to import. Each selected series becomes its own top-level type, such as Weight or Resting Heart Rate. When importing one series, use --type Health --subtype Weight to place every measurement under a specific type and subtype; --subtype cannot be used without --type.
Imported values are encrypted locally inside row notes. Deterministic row IDs make repeated imports safe and automatically collapse exact duplicate records.
The new Measurements report in Statistics turns numeric rows into private value-series charts. Choose a type and, when available, a subtype such as Health → Weight, Heart Rate or Body Fat. Rows shows the latest value, change, average, observed range and measurement count.
Switch between raw observations and weekly, monthly or yearly summaries. Charts use clear whole-number value scales and show every month in shorter date ranges. Each period includes its average, minimum, maximum, first value, last value and change. Missing periods are omitted rather than estimated.
A row becomes a measurement when its encrypted note starts with a number and optional unit, such as 77.6 kg. This includes rows created by the Apple Health importer as well as values entered manually. Notes are decrypted and analyzed only in your browser; the server never sees the measurements.
Log a row whenever you glance at the odometer — category Km, subcategory = the vehicle, note starting with the reading — and the new Mileage report in Statistics turns those rows into km per year and per month, per vehicle and total.
Works for anything that only counts up: electricity meters, bike odometers, book pages.
On mobile browsers, tapping into a note's editor (or the search box) made the page zoom in and shift around. This was iOS Safari's auto-zoom kicking in for text fields smaller than 16px — the same reason Rows editing was already immune. Notes fields now use 16px on mobile, so the layout stays put while you type.
A batch of Notes improvements:
A note could wrongly warn that it was edited on another device — and then lose your last edit — even when it was only open in one place. This happened when two auto-saves overlapped, or when a save's write reached the server but its response was lost (a brief network blip). Saves are now serialized so they can't race, and a version mismatch is reconciled against the server before assuming a conflict: if the server already holds your own content, the save simply catches up instead of warning. The conflict banner now appears only for a genuine edit from elsewhere.
While editing a note, the browser sometimes stored line breaks in a way that made a single edit merge several lines into one, drop text from the start of a line, or lose the indentation Enter should carry. The editor now reads and writes line breaks consistently no matter how the browser structured them, so lines and indentation are preserved. Covered by tests reproducing each case.
In a very long note the browser was re-spellchecking the entire note on every keystroke, which made typing and pressing Enter lag. Spellcheck now turns off automatically for large notes, so editing stays responsive. Normal-sized notes keep spellcheck.
Selecting text and pressing Tab (or the indent button) used to replace the selection with a single tab — so the selected lines vanished. Now Tab adds an indent to the start of every selected line, and Shift+Tab (or the outdent button) removes one level from each, with the selection kept so you can keep pressing. A plain cursor still just inserts a tab.
Two ways a note could lose recent changes when you switched away from it are fixed. First, to keep big notes typing smoothly the editor saved its text a moment after you paused — and switching in that window could grab the older text; it now always captures the current content first. Second, and more seriously: if you pasted images and switched (or left Notes) before they finished uploading, those still-uploading images were dropped from the save — so freshly-added images at the top of a note could vanish. Saving now waits for in-flight image and file uploads to finish first. Image-heavy notes also load a little more smoothly.
Opening a note packed with large images used to decrypt and decode every image at once, freezing typing for a few seconds. Images now load lazily — only the ones near what you're viewing are decoded, the rest as you scroll to them — so even a note full of pasted images opens smoothly.
If the same note was open in two tabs or on two devices, whichever saved last could silently overwrite the other's changes. Now each save carries the version it started from, and the server rejects a save that would clobber a newer copy. Instead of losing work, you get a clear banner — "this note was changed in another window" — with a Reload button to pull the latest.
The note editor toolbar now has indent (⇥) and outdent (⇤) buttons, so you can change indentation even on a phone — the iOS on-screen keyboard has no Tab key. On a physical keyboard, Tab indents and Shift+Tab outdents.
Moving a note to another folder (or reordering it) no longer changes its "last modified" date — only real edits to the title or text count as a change, so notes don't jump to the top of the list just for being filed. The note view now also shows the Created date next to Updated.
The terminal app now keeps a few blank lines below the newest rows instead of letting them sit right against the bottom edge. Just like the web app, that little gap makes it obvious at a glance when you've reached the end of the list — and any rows that sync in from another device show up clearly in the space below your cursor.
Writing indented notes now behaves the way you'd expect. Tab inserts a tab (it used to just move focus out of the editor), Shift+Tab outdents, and pressing Enter keeps the current line's indentation, so nested lists and indented blocks stay lined up. Shift+Enter gives you a plain, unindented break.
The Notes view was built as three side-by-side panes, which didn't fit on a phone. On mobile it's now a clean drill-down: you start on your folder tree, tap a folder to see its notes, tap a note to read or edit it full-screen — with a Back button at each step. Rows and Notes are now two worlds you switch between with a small toggle at the top, and the bottom bar gets out of the way while you're in Notes.
Switching from Rows to Notes used to blank the screen and reload the whole library every time. Notes now keeps its view in memory — like Rows already did — so coming back is instant, showing your last folder and note immediately while it quietly refreshes in the background. The Notes view is also prewarmed while you're on Rows, so even the first switch is fast. And the note list now loads a page at a time — only the notes you actually see — fetching more as you scroll, so a notebook with thousands of notes opens just as fast as a small one. Searching still covers everything: it pulls the full notebook behind a progress bar (your notes are end-to-end encrypted, so the search runs on your device).
On mobile, switching from Rows to Notes no longer makes the header jump upward. The Notes view now reserves the same top safe-area space as the Rows view, so the Rows/Notes toggle and everything below it stay in exactly the same position when you flip between the two worlds.
Editing a very long note (hundreds of KB) used to lag on each keystroke — pressing Enter or typing took a noticeable beat. The editor was re-serializing the entire note to markdown on every keypress. It now defers that work until you pause, and does it more cheaply, so even big notes type smoothly. Your edits still save the same way.
In the terminal client, the delete confirmation now names the exact row — its date, time, and activity — instead of a generic "Delete this row?". The row to delete is locked in the moment you press Delete, so if a background sync pulls in rows from another device and reshuffles the grid while the prompt is open, the delete still removes the row you picked rather than whatever slid under the cursor.
Opening the map used to load every location you've ever saved, which could take a while and pull in a lot of data. The map now starts on today's locations and loads quickly — you can still switch to a week, month, year, or all time whenever you want the bigger picture.
Rows you created on the web or another device wouldn't appear in the terminal client (TUI) while you had it open — the grid only ever refreshed when you started editing a cell or restarted the app, so new entries from elsewhere seemed to never arrive. The TUI now checks the server for changes in the background and, while you're viewing your most recent rows (the bottom of the grid, where the cursor sits by default), quietly pulls them in within a few seconds and drops to the new entries so you can see them. It holds off while you're mid-edit or scrolled up reading older history so your place is never yanked, and you can force a refresh at any time with F5 (or Ctrl+R). The build identifier is now shown in the top bar so you can tell exactly which version you're running. Claude's local cache, which the TUI also keeps in sync, had a related issue where its sync marker could skip rows created on other devices; it now tracks the server's own timestamps so those entries are no longer missed.
Adding a row while your connection briefly stalls could queue it for sync without you noticing. If the app then refreshed the list from the server before that row finished syncing, it would disappear from the view until you reopened the app. Rows with edits still waiting to sync are now kept on screen through a refresh, so what you just saved stays visible.
On networks that connect but don't actually reach us — airplane or hotel WiFi that only allows a few apps, captive portals — your device reports itself as online, so the app used to keep trying to reach the server and never switched to offline mode. We now actively probe reachability with a short timeout, so the app drops into offline mode promptly: your changes are saved locally and queued to sync, and the offline banner appears as it should.
Fixed a problem where rows added while offline (for example, on a plane) wouldn't appear in the list — and stayed hidden even after reopening the app — until you reconnected. The app now rebuilds the rows view from your local offline copy when there's no connection, so newly added rows are visible immediately. Your changes were always saved safely and synced on reconnect; this fixes only the display.
You can now create, edit and delete rows (and GPS points) with no connection. Changes are saved to a local store and queued; the moment you're back online they sync automatically, and edits made on your other devices flow back in. Notes search also works offline now, instantly, against a local mirror of your data. Conflicts use last-write-wins, and the full edit history remains available for recovery.
The installed app now launches without a connection. Previously, opening rows.life on an iPhone home screen while offline showed a blank screen — even though your data was cached locally, the app shell itself never loaded. The service worker now caches the app shell document and serves it for offline launches, so you can open the app and browse your cached entries with no signal.
Excel export still defaults to exporting everything — a full backup — but you can now narrow it down. In Settings → Export, with Excel selected, tick Filter what to export to choose a date range and a category (optionally a single subcategory). Picking a parent category includes all of its subcategories. Handy for pulling just a client's hours or a month's entries into a report without hand-trimming the spreadsheet afterwards. SQLite export is unchanged and always exports the complete database.

The map could stall when opening a range with many thousands of GPS points (e.g. after importing a long track). It now opens smoothly: the "furthest apart" stat skips a brute-force pass, marker pins are built and inserted in bulk, and pin details are rendered only when you actually click a pin.
The Jump to heatmap on the map could show a non-zero count for a week or month that had no points to display — clicking the cell opened an empty map. This happened when a row had GPS points added manually (or imported) whose own timestamps fell outside the row's day: the heatmap counted those points under the point's timestamp, while the filter that loads the map fetches rows by the row's start time, so the two disagreed. The heatmap now buckets every point by its parent row's start time, matching what the map actually renders when you click the cell.
When you share a photo library with someone — partner, family — and you both have the same iPhone model, rows import-gps used to lump everyone's photos under the same device label ("Apple iPhone 14 Pro"), so --device couldn't separate them.
The importer now also reads the EXIF HostComputer field, which is where Apple's Camera app writes the device's user-set name (the one in Settings → General → About → Name). When present, it gets appended to the device label, so two same-model phones show up as separate entries in the device summary and you can filter them apart:
Devices found:
1,243 Apple iPhone 14 Pro — Pablo's iPhone
872 Apple iPhone 14 Pro — María's iPhone
48 Apple iPhone 14 ProThen --device "Pablo" (case-insensitive substring) imports only yours.
There's also a new --note-device flag. Used together with --create-missing, it appends a line to each new entry's notes saying which device(s) the photos came from — e.g. "Pictures taken with Apple iPhone 14 Pro — Pablo's iPhone". Handy when you import a partner's photos and want the entry itself to remember whose camera it was, without rummaging through metadata later. The preview output shows the device suffix too, so you can see what the notes will say before committing the import.
Two caveats on the HostComputer side. It only works for photos, not videos — Apple's .mov metadata doesn't carry the device name. And it only helps if the phones have non-default names: if both are still called "iPhone", there's nothing to distinguish. Renaming a phone affects future photos only; historical ones keep whatever was set when they were shot. Photos that came through metadata-stripping services (WhatsApp, Telegram) won't carry HostComputer either.
Named locations (your @home, @office, …) are the one place coordinates were historically stored unencrypted on the server, while everything else — notes, GPS points, category names — has always been encrypted in your browser before it leaves your device. We've been migrating those anchor coordinates under the same zero-knowledge encryption; each client quietly re-encrypts them the next time you log in.
This release closes the remaining gap: once a location's coordinates have been encrypted, the API stops returning the old plaintext copy entirely — it's dead weight the apps already ignore. Plaintext is now sent only for locations that haven't been re-encrypted yet, and solely so the migration can finish the job. We also fixed an edge case in the desktop/TUI client where a half-written record could be mistaken for "already encrypted" and skipped, leaving its plaintext stranded.
No action needed on your part, and nothing changes in how locations look or behave.
On phones, picking a specific scope (Day, Week, Month, Year) made the date stepper push the toolbar wider than the screen — the next-day arrow and the Area button got clipped off the right edge, so there was no easy way to draw an area filter. The toolbar now stacks cleanly: the scope pills and the date stepper each take their own full-width row, the date label truncates instead of wrapping, and the Area button (with its label) stays fully visible next to the category filter.
The new Filter by area rectangle didn't work on touch devices — drag-to-draw conflicts with single-finger pan, so no rectangle would ever land. Touch now uses a two-tap flow instead: tap the Area button, tap one corner of the area you want, tap the opposite corner, done. A small red dot marks the first corner so you can see what's coming, and pan/zoom stay enabled between taps so you can reposition the map.
Two related fixes on phones:
Desktop drag-to-draw is unchanged.
A new Area button in the map toolbar lets you drag a rectangle on the map. Once drawn, only locations inside that rectangle show up — date scope and category filters still apply on top, so you can ask things like "how many days did I spend in London in 2023?" with one selection.
A Locations panel appears alongside the map listing the distinct days you've been in the selected area, with the per-day pin count. Click a day to expand its entries; click an entry to pan the map to that point. The headline number — N days in area — is the simplest honest answer to "how often have I been there?"
On mobile, the panel becomes a second tab next to the map (only visible while a filter is active). The mobile toolbar also got a small layout fix: km apart no longer gets clipped at the right edge of the screen.
Press Esc while drawing to cancel; click the Filtered by area chip in the toolbar to clear an active filter.

Settings has a new New rows tab that turns previously hardcoded behavior into preferences that sync across devices.
The default still applies — when you add a row at the bottom of the sheet and a long gap has passed since your last entry, the new row jumps to the current time instead of continuing from where the previous one ended (introduced in PR #12 with a hardcoded 5-hour threshold). You can now:
New: opt in to set end time to current time on new rows. Off by default. When on, every new row is created with both start and end already filled in — useful if you mostly track tasks you've just finished.
Everything is stored per-account and applies on web today, with TUI and mobile to follow.

When you have Set end time to current time on new rows turned on, pressing Enter repeatedly on a blank row at the bottom of the sheet used to create a stack of zero-duration rows with timestamps marching into the future and the focused row jumping a couple of positions up between presses.
Now: if the bottom row has no category, no subcategory and no notes, pressing Enter is a no-op — the cursor stays put on the row that's still waiting to be filled in. Once you've filled in any of those three fields, Enter creates a new row again as before, and the new row's end time is clamped so it never sits before its start.
Adding the new New rows tab pushed the Settings tab bar past the edge of the screen on phones — the rightmost tabs (and the page content) ended up cut off. Same thing was already happening to the Admin Analytics page once it grew to seven tabs.
On narrow viewports, tabs now wrap onto a second row with a pill-style active highlight, so every section is reachable without horizontal scrolling. Desktop layout is unchanged.
The map toolbar now shows a small X km apart stat right next to the pin count — the great-circle distance between the two furthest GPS points in whatever date range you have selected. Switch from Year to Month to Week and watch it shrink as the trips fall out of view.
It's the simplest honest answer to "how far did I get this year?" Picking diameter rather than path-length sidesteps the trap of pretending we know the route between sparsely-tagged points: a single Madrid pin and a single Tokyo pin from the same trip would otherwise read as a 10,800 km straight-line jump even though we have no idea what was traveled in between. X km apart just states the geographic fact about two points the user actually tagged, with no claim about routes.
Hidden when there are fewer than two points or the spread is under 1 km — a stay-at-home week stays clean, a trip across Europe lights up.

The in-app messages system now supports single-choice questions, not just announcements with a CTA button. When a message has options attached, the popup renders them as chips — picking one records the response and dismisses the message in a single click.
First use of it: a one-question survey for users with 5+ days of tracking, asking whether they'd prefer the End column to fill automatically with the current time when creating a new row.
The map's date filter has been rebuilt around a single question: which week should I be looking at? The old All time dropdown is gone; in its place there's a scope bar with All · Year · Month · Week · Day pills, a prev/next stepper for the current scope, and a new heatmap popover for jumping across years.
Scope pills + stepper. Pick the granularity once; the ◀ label ▶ arrows step ±1 of whatever scope you're in — next week, previous month, next year. One click, no re-selection.
Heatmap popover for big jumps. Click the label to open a 12-month × 5-week grid for the selected year. Each cell is colored by how many GPS entries landed in that week — so a week spent in Madrid lights up dark, a quiet week at home stays pale. The exact count is printed inside every cell (847, 2.4k, 16k) so you can triple-check what you're looking at.
Day scope swaps the heatmap for a regular month calendar — click any day to filter to it, including a Today shortcut.
Quieter data loading. Navigating years inside the popover is staged — the map only reloads when you actually apply a selection, so scrubbing back through 2014 → 2015 → 2016 doesn't trigger a cascade of fetches. Each year's heatmap density is fetched once and cached.
The heatmap counts individual GPS points (not rows), so it matches exactly what the map shows when you zoom in — a row with a full day of auto-tracking contributes all of its points to the week, not just one.

Ctrl+S to save and close (previously Ctrl+Shift+Enter, which caused issues with some terminal emulators). Enter continues to insert newlines for multi-line notes.Two quality-of-life improvements for the terminal client:

Adding a location to an entry that already had one resulted in duplicate GPS points with no way to remove them. The agent and MCP integration now support a delete_locations tool that removes all GPS points from an entry.
To fix a wrong location: ask the agent to delete the location, then add the correct one.
You can now save frequently-used places like @Home, @Office, or @Gym and reference them with a simple @ mention in your notes. Selecting a location automatically attaches GPS coordinates to the entry.
Three ways to create named locations:
@ autocomplete everywhere:
@ in the notes field (rows view, edit dialog, or mobile) and a dropdown appears with your saved locations





The rows agent and MCP integration can now edit existing entries and add GPS locations — two capabilities users had been asking for.
The delete row shortcut on macOS is now ⌘+Backspace, matching the standard macOS convention. Previously it required ⌘+Delete (forward delete), which many Mac keyboards don't have as a dedicated key. Windows and Linux shortcuts remain unchanged (Ctrl+Delete).
You can now delete time entries through the MCP integration and the rows agent. Claude can find and soft-delete entries on your behalf — deleted entries can be recovered within 30 days.
Use search or filter tools to find the entry first, then ask Claude to delete it.
New users now see a Take the tour button when they first log in, instead of a blank screen. The tour loads real demo data (2 years, 4,260 entries from a fictional Czech programmer) and walks you through every feature in 12 steps:
The tour works on both desktop and mobile, with adapted layouts for each. On mobile, stats panels slide in above the tour card so you can see both. Navigate with Next/Back buttons or arrow keys.
A tour icon in the sidebar lets you restart it anytime — it pulses red if you have fewer than 15 days logged, gently reminding you to explore.

Introducing rows agent — a conversational AI assistant that lives in your terminal and turns your rows.life data into a queryable personal memory. This is an early proof of concept that connects directly to the Anthropic API.
What it can do:
Smart enough to reason: With Claude Sonnet, the agent can distinguish between talking about Madrid in your notes vs. actually being there, cross-referencing GPS data with note content to give precise answers.
How to try it (requires your own Anthropic API key):
$env:ANTHROPIC_API_KEY="sk-ant-api03--your-key"; $env:ROWS_AGENT_MODEL="claude-sonnet-4-6"; rows agent
This is a proof of concept — the final product won't require you to bring your own API key. We're using it to learn what makes a great AI-powered life memory interface.




s from the table view to enter Stats mode with four tabs: Year in Review (bar chart of hours by category with drill-down into subcategories), Categories (browse all categories with all-time stats and subcategory breakdown), Overall (year-over-year comparison bar chart with summary table), and Trends (line chart with configurable date range, granularity, and category picker dialog). Navigate years with arrow keys, switch tabs with 1/2/3/4, drill down with Enter, and press c to select trend categories.

Delete or Backspace on any row and confirm with y — any other key cancels. The status bar shows the keybinding hint and a red confirmation prompt so you won't delete anything by accident.m on any row to see its location points on a true-color Mapbox map rendered with half-block pixel art. Pins are color-coded by category, and pressing n/p cycles through markers with a label showing time and category. Press a to toggle between the selected entry's locations and all your locations (fetched live from the API). Rows with GPS data show a ● indicator in the table so you can spot them at a glance. Pan with arrow keys, zoom with +/-, and switch between six map styles with s.
rows import-gps, this means every photo you took during an entry automatically becomes a pin on the map — so a 3-hour bike ride doesn't collapse to a single dot, it becomes a trail of everywhere you stopped to take a picture.The rows import-gps command now uses real distance calculations (haversine formula) to deduplicate GPS coordinates. Photos taken within 50 meters of each other on the same entry collapse into a single point — so ten selfies at the same restaurant become one pin, not ten. You can safely re-run the import on the same photo directory: existing coordinates are checked by distance, and only genuinely new locations are added. This also means importing overlapping photo libraries (e.g. iCloud copies + local backups) won't create duplicate pins.
The --create-missing flag is now much smarter. Instead of creating one row per day, it clusters photos by time: if there's more than a 1-hour gap between consecutive photos, it starts a new entry. Each entry gets the proper start and end times from its photo cluster, all GPS points (deduplicated within 50m), and a place name automatically resolved via OpenStreetMap reverse geocoding — so instead of a blank row, you get something like "Calle Mayor, Salamanca" as the note. The entries are created under the 'Location' category.

You can now enrich your entries by adding GPS locations extracted from your photo collections! Since photos and videos contain GPS coordinates in their EXIF metadata, the new rows import-gps command reads them and adds the locations to your rows.
For example:
$ rows import-gps C:\Users\pablo\Pictures\2021\ --device "Apple iPhone 7" --device "Apple iPhone 13" --previewThis scans all photos and videos in the directory, filters by the specified devices (your phones, not your family's), and shows a preview of which entries would be enriched — without changing anything. The command displays a summary of all camera devices found, so you can decide which ones to include.
Remove --preview to actually apply the changes. Photos are strictly matched to rows by time window: a photo only enriches a row if it was taken during that row's time range. Existing locations are never overwritten.
Use --create-missing to also create new 'Location' entries for dates where you have GPS-tagged photos but no matching row. Without it, only existing rows are enriched.
Supports JPEG, HEIC, PNG, TIFF, MOV, and MP4 files.



