Google sync no longer downloads a contact's photo again when it hasn't changed (CL-0088)
vCard export writes birthdays, addresses and organizations as the standard fields other contact apps read (CL-0075)
Files exported by earlier versions still import.
google-auth-httplib2 is capped at its current minor (0.4.x), so an untested breaking release can no longer install silently (CL-0082)
Deleting many contacts at once is now one save instead of one per contact, and is capped at 10,000 (CL-0068)
CSV and vCard exports now stream to the browser instead of building the whole file in memory (CL-0067)
The vCard export also loads every contact's custom fields in one
database query instead of one query per contact.
The contact list no longer re-derives its index keys on every render (CL-0066)
The alpha-nav counts and the letter filter grouped on a Python callback SQLite cannot index, and duplicate detection re-parsed every stored phone number in Python. Both keys are now computed once when a contact is saved and held in a new indexed contact_lookup table, so each is an index lookup. Query plans confirm it: the counts, the letter filter, the phone lookup and the phone grouping all now use an index where every one of them previously scanned the whole table.
Replaced an email index nothing could use, and dropped one nothing reads (CL-0066)
idx_contacts_email indexed the raw column while every lookup wrapped it in LOWER(), so it cost a write on every save and served no query; it is now an index on the LOWER(email) expression, which the duplicate scan does use. idx_contacts_phone had one remaining reader, and this change removed it, so that index is gone too.
Starting the app no longer opens a browser tab; the tray icon is the way in (CL-0060)
A start whose tray icon appears now opens nothing. Two cases still open the page: launching a second copy while one is already running, and a start where the tray could not appear at all (a desktop with no system tray) — without that fallback the app would be running with no icon, no tab and no visible address.
A busy port that PORT named explicitly is now a failure, not a hand-off (CL-0056)
When PORT names a port and something already holds it, the launcher
exits non-zero and opens no browser instead of handing off to the
existing instance. Without PORT the hand-off is unchanged.
Exported vCards now fold long lines and give company cards the name field strict readers require (CL-0075)
Screen readers now hear the import and merge controls' names, the current sort order, and how many contacts are selected (CL-0084)
Keyboard shortcuts also no longer fire when Ctrl, Alt or Cmd is held.
A form that fails to save keeps what you typed: an invalid custom field stays on the page, and a ticked "Remove photo" stays ticked (CL-0079)
Merging contacts now checks the email and formats the phone number the same way the contact form does (CL-0079)
The birthdays page shows dates in the format you chose in Settings (CL-0079)
On the duplicates page, card view lays out every group, not just the first (CL-0079)
A delete confirmation can no longer be skipped by clicking the same button twice (CL-0079)
Restart relaunches the app from the right place in the downloadable builds (CL-0063)
The Linux download relaunched itself from a temporary folder that disappears as the old copy closes, so Restart most likely just closed the app, and at best brought back the old version after an update. It now relaunches the AppImage file itself. Every packaged build also starts the new copy as a clean, independent program. Not yet tried on a real AppImage build.
Restart can no longer leave the app closed if the new copy starts too quickly (CL-0054)
A relaunched copy that started before the old one had let go of its network port decided the app was already running and quit, so nothing was left open. The relaunched copy now waits up to five seconds for the old one to finish.
A contact deleted on Google no longer lingers here pointing at nothing (CL-0070)
If you had edited a contact here and it was deleted on Google before the next sync, the sync skipped the deletion and Google never sent it again. The contact stayed here, still linked to a Google contact that no longer existed. Now your edited copy is kept and put back on Google in the same sync. A contact you had not edited is removed here, as before.
A network hiccup no longer asks you to reconnect to Google (CL-0070)
When the app couldn't reach Google to renew its sign-in, it treated that as a lost connection, so a brief outage sent you to reconnect. Now the Sync page stays as it is and a sync says "Couldn't reach Google. Check your connection and try again." You are asked to reconnect only when Google has actually refused the sign-in.
Google sync rides out rate limits instead of skipping contacts (CL-0070)
When Google said "too many requests", every contact after that point was skipped. That is most likely on the first two-way sync, which uploads every contact that exists only here. Each call to Google now waits and retries, taking longer between attempts. A rate limit that outlasts the retries is no longer reported as a lost permission that needs reconnecting.
Photo downloads no longer lock the database during a sync (CL-0070)
Each page of contacts downloaded its photos while holding the database's write lock, so a slow photo server kept every other save waiting. Photos are now fetched after the page is saved. Each download also has a total time limit, so a server sending bytes very slowly cannot stall the sync.
Clearing a contact field now reaches Google instead of coming back (CL-0064)
Emptying an email, phone or note on a synced contact used to be dropped from the push, so Google kept its copy and the next sync put it back. The clear is now sent, and any additional values Google holds for that field are left alone. Deliberately limited to those three: for a name, birthday, address or company organization an empty local value does not reliably mean you cleared it, and pushing one would delete something you never touched.
Screen readers now announce what a confirmation dialog is asking (CL-0073)
Every destructive action uses one confirmation dialog, and it announced only "Confirm, button" without reading the question. It is now a proper dialog: the message is its name, focus returns to whatever opened it, Tab stays inside it, and keyboard shortcuts no longer fire underneath it — pressing "n" at a delete prompt used to navigate away and leave the dialog hanging.
The duplicates and birthdays pages bound their results and say when they are truncated (CL-0066)
Both returned every match, against the documented rule that all list endpoints bound their output. Each now shows at most 200 and says so when there is more, rather than presenting a partial list as if it were complete.
Changing the phone region now re-derives every contact's stored phone key (CL-0066)
The E.164 form used to match duplicates depends on the phone-region setting. Without this, changing that setting would leave every stored key derived under the old region and duplicate detection would quietly stop matching numbers typed in local form.
Google Sync works in the downloadable app (CL-0080)
Opening the Google Sync page in a packaged build showed an error page
instead of the page. A library the sync code needs was never included in
the bundle, so the feature could not work there at all — while running
from source was unaffected, which is why no test caught it. Found by
launching a real build and opening the page.
Imported files can no longer store oversized contact fields (CL-0068)
Name, email, phone, notes and custom-field values are now length-checked
where the data is written rather than only in the browser, so an imported
CSV or vCard gets the same limits the contact form shows. The
fifty-custom-field limit applies to imports too; it previously applied
only to the form.
Replacing a contact photo no longer risks losing the old one (CL-0071)
The previous photo was deleted before the new one was written, so a
failure in between left the contact with no photo at all and a broken
avatar. The new photo is now put in place first.
Two copies of the app starting at once agree on one session key (CL-0069)
On a first run they could each generate a different key, and the loser
rejected every form submission. An unreadable key file also stopped the
app starting with no message anywhere; it now logs and creates a new one.
A photo that cannot be saved no longer looks like the contact failed to save (CL-0079)
A full disk raised an error page after the contact had already been
stored, so the user could not tell it had worked. It now says the contact
was saved and the photo was not.
Back-to-top and the sticky filter bar work on every page (CL-0048)
Both were stranded behind an early return that fires on every page
except the contact form, so the button never appeared where it was
useful and the filter bar fell back to a fixed offset. The scroll to top
now also respects a reduced-motion preference.
The system-tray icon now appears on Windows and macOS
The appindicator backend was pinned on every platform, not just Linux.
pystray imports the named backend unconditionally and does not fall back,
and that backend needs a Linux-only library — so the tray failed to start
on Windows and macOS, and the app then opened a browser tab on every
launch as its no-tray fallback. Both platforms now select their own
native backend, as the design document always said they would.
Multi-line notes survive a vCard export and re-import
Notes typed on more than one line were exported with a raw carriage
return, which acted as an end-of-line marker when the file was read back
— so everything after the first line was lost. This also closes a way for
imported contact data to inject extra entries into an exported file.
Emails and phone numbers with custom labels now import from Google and Apple exports
Both write those entries with a group prefix on the property name, which
the parser did not recognise, so exactly the labelled emails and phone
numbers were dropped without a word.
A vCard import that skips contacts now says so
Contacts the importer refused were counted as neither imported nor
skipped, so a file where most records failed still reported an
unqualified success. The summary now reports the skipped count and the
reason for each, as the CSV import already did.
Dates and the timezone setting work in the Windows build
Windows ships no system timezone database, so the frozen build had none:
the Settings timezone list rendered empty, saving a timezone always
failed, and every date in the app fell back to a raw machine-readable
timestamp. The database is now bundled into the Windows build.
The delete confirmation counts contacts, not checkboxes
On the duplicates page a contact can appear in more than one group, so
"delete 5 selected" could precede a delete of three.
Ctrl-C stops the server instead of hanging it
The server ran on a thread nothing shut down outside the tray's Quit, so
interrupting a run from the terminal printed an error and then hung,
still serving, until the process was killed.
Google sync no longer discards its own last-sync time when recovering
When Google retired a sync token the recovery cleared the whole sync
record rather than just the token. If the restarted sync then failed
part-way, the next run could not tell which contacts had local edits
pending and let Google's copy overwrite them.
Pushing a contact to Google preserves birthday entries it does not manage
The birthday was the one field written as a wholesale replacement rather
than an in-place update, so anything else Google held there was
discarded on every push.
A Google sync that fails for want of permission now says so
A refused write was counted as an ordinary per-contact skip, so a token
that had lost its write permission reported every contact as skipped
with no hint that reconnecting was the fix.
System-tray icon now appears when running from source or from a self-built AppImage, not only in CI-built releases (CL-0057)
The GI/AppIndicator stack was installed only in the release workflow, so ./run.sh and a local packaging/build-linux.sh both produced a tray-less app — the latter exiting 0 while logging "Hidden import 'gi.repository.DBus' not found". run.sh now builds its venv with --system-site-packages (rebuilding an existing venv once, since the flag is fixed at creation), and build-linux.sh refuses to build under an interpreter that cannot load the GI typelibs rather than silently shipping without a tray. Linux from-source users need a few distro packages — see the README.
A non-numeric CONTACT_LIST_PORT no longer crashes at startup (CL-0056)
It warned nowhere and raised an unhandled ValueError while importing
config; it now logs a warning and uses 5002. Its accepted range is
unchanged.