Skip to content

1.12.0 - #3312

Merged
Freika merged 133 commits into
masterfrom
dev
Aug 11, 2026
Merged

1.12.0#3312
Freika merged 133 commits into
masterfrom
dev

Conversation

@Freika

@Freika Freika commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Release 1.12.0 — everything merged to dev since 1.11.0. 129 commits, 811 files, +38,444/−27,125.

Added

Removed

Changed

  • Visit detection v3 — rebuilt around stays, movement, and honest gaps. A tracking silence that starts and ends at the same place counts as one continuous visit; a silence that ends somewhere else becomes an explicit "untracked" stretch. Detection cross-checks transportation segments (no more fake roadside "visits" from slow traffic), visit start/end snap to when movement actually stopped, and business/POI names are only used when the evidence supports them. Every visit carries a confidence score: medium-confidence visits render subdued, low-confidence ones collapse into a per-day disclosure. (Visit detection rewrite: bridged stays, movement reconciliation, honest gaps #3329)
  • Visit lifecycle — the review flow is gone (suggestions banner, "Needs review" cards, Confirmed/Suggested filters, bulk Confirm); every visit row has an Edit button, and editing a suggested visit confirms it. Deleting a visit keeps an invisible tombstone so detection never re-suggests it. Machine-detected visits are regenerable wholesale; anything confirmed, renamed, re-placed, deleted, noted, or imported survives re-detection untouched. The "Visit Max Gap" slider and "Smart density fill" toggle are removed. Area visits are no longer a separate nightly pass — areas label detected visits, and reshaping an area relabels its historical visits. (Visit detection rewrite: bridged stays, movement reconciliation, honest gaps #3329)
  • Transportation mode detection — rebuilt on movement patterns over time windows (speeds, heading changes, stop patterns) instead of point-by-point rules; segments carry confidence scores and tracker hints are blended in rather than trusted. Recognizes stationary, walking, running, cycling, driving, train, flying (bus/boat/motorcycle only via tracker hints or manual set). Threshold sliders and expert mode are removed. (Transportation mode detection rewrite + flattened timeline UX #3302)
  • Timeline UX — single-line day header with per-mode distance chips plus moving time; a track expands to a mode ribbon with only the legs that matter; a leg's mode is editable in place; the full segment list lives behind "All segments". The interior "untracked" bar threshold drops from 90 to 45 minutes once history is re-detected. Journey rows ("walked", "drove", "flew") are translated. (Transportation mode detection rewrite + flattened timeline UX #3302)
  • Days per Country — moved from the map's Create tab to the Insights page as a pair of cards (countries with stay periods, year heatmap) following the page's year picker. (Move Days per Country from the map to Insights #3333)
  • Turbo morphing — deleting an import or a notification updates the list in place instead of repainting the page. (Adopt Rails 8.1 defaults, resumable backfills, and Turbo morphing #3297)
  • Resumable backfills — long-running history backfills resume where they stopped after a restart instead of starting over. (Adopt Rails 8.1 defaults, resumable backfills, and Turbo morphing #3297)
  • Monthly statistics maps and publicly shared month maps now render with MapLibre GL. Top navigation collapses into the menu button below 1280px so longer translations fit. Rails 8.1 framework defaults.

Fixed

Internal

Upgrade notes (self-hosted)

  • Restart the app once migrations have run — until restart, the running process's stale schema cache makes deleting a visit look like it worked without saving.
  • Migrations enqueue background work on the lowest-priority queue: a track-segment time-anchoring backfill and a fleet-wide visit re-detection. Set SKIP_VISITS_FLEET_REDETECT=1 before migrating to opt out of the latter (then run Visits::FleetRedetectJob.perform_later manually when convenient).
  • Existing visits and tracks keep their old detection until re-detected: Settings → Visits (re-detect button) and Map settings → Transportation Mode Detection ("Re-classify my history"). Fleet-wide track reclassification is console-only: TransportationModes::FleetReclassifyJob.perform_later after the segment backfill finishes. Dawarich Cloud is re-detected in a staged rollout.
  • Version: 1.11.0 → 1.12.0.

sylvainmetayer and others added 30 commits August 2, 2026 23:12
Centralising copy folded distinct strftime literals onto shared I18n
format names, which changed rendered dates at eight call sites, and
switched the location search formatter to a lang-derived locale that
resolves to en-US and swaps day and month.

- Add month_day_year, month_day_padded, short_month_day_padded and
  medium_padded so the call sites needing a different padding keep it
  without disturbing the sites that share the original format names.
- Correct day_month_year, human, human_with_seconds and long_with_time
  in place, since each has a single consumer.
- Pin the location search formatter back to en-GB so 3 August renders
  as 03/08/2026 rather than 08/03/2026.
- Add the four missing battery_status enum values; the family map popup
  rendered the raw key for anything other than charging or full.

Cover both with regression specs and a JS test for the date ordering.
chore: extract UI strings to Rails I18n
Bump load_defaults from 8.0 to 8.1 and drop the now-vestigial
new_framework_defaults_8_0.rb: all three of its settings, including
Regexp.timeout, are already applied by load_defaults 8.0.

Checked the two behaviour-changing defaults before flipping.
raise_on_missing_required_finder_order_columns only fires on models with
no primary key, query_constraints or implicit_order_column, and every
model here has an id. action_on_path_relative_redirect = :raise leaves
the single bare-string redirect_to in the app untouched, since it is an
absolute MANAGER_URL passed with allow_other_host.

YJIT is unaffected: the container already sets RUBY_YJIT_ENABLE=1.
Rails 8.1 ships ActiveJob continuations and sidekiq 8.1 implements the
stopping? hook they need, so a job interrupted by a deploy can now
checkpoint and pick up where it stopped instead of restarting from the
first row.

Adds a Resumable concern and moves the five batch-walking jobs onto
step/cursor. The altitude backfill had already hand-rolled a last_id PK
cursor; that is now the step cursor. The stats rebucket previously
re-enqueued every Stats::CalculatingJob it had already fanned out when
it was restarted.

Two things worth knowing for anyone adding a job here:

Resumable sets resume_errors_after_advancing = false. The Rails default
of true turns any StandardError raised after the cursor has advanced
into a silent self-requeue, which stops the exception reaching the
caller. That broke the failure path of
DataMigrations::RecalculateAnomaliesUserJob, which calls
Points::AnomalyBackfillUserJob and branches on the result. Resumption is
for interruptions, not for errors.

Splitting a job into steps can duplicate a destructive prologue.
AnomalyBackfillUserJob clears anomaly flags before re-evaluating them,
so the reset is its own step and is skipped on resume; otherwise every
resumption wipes the flags it had just recalculated.

Deliberately not converted: EnhancedImport::ExtractJob,
Tracks::BoundaryResolverJob and
DataMigrations::RecalculateAnomaliesUserJob. Their requeues are lock
contention backoff, chunk-completion polling and dispatcher claim
release. Continuations resume interrupted work; they do not help a job
that is waiting on something else.
Turbo 8 can morph the body on a page refresh instead of replacing it, so
a same-path redirect keeps scroll position and leaves the surrounding
DOM alone. Deleting an import or a notification now updates the list in
place rather than repainting the page and jumping to the top.

Opt-in per controller rather than on globally, because Chartkick is not
compatible with morphing: it destroys every chart on turbo:before-render
and morphing does not re-run the inline scripts that construct them, so
stats, insights and digests would refresh into blank charts. Those stay
on replace. Map pages are already excluded by their own layout.

turbo_refreshes_with writes into the :head content buffer, which the
application layout was not yielding, so the layout now does.

Note that morphing only applies to same-path visits with a replace
action. A browser reload is a full document load and is unaffected. The
main form on settings/general carries data-turbo: false, so saving
settings still does a full load; only the smaller turbo-enabled forms on
that page morph today.
Add explicit locale selection and persist user preferences without mutating unrelated settings. Propagate locale choices to mail and background notifications.

Correct German UI copy, sentence structure, relative times, and digest labels, with regression coverage for the affected flows.
Make French selectable and persist locale preferences across authentication, mailers, jobs, and JavaScript-rendered UI.

Polish user-facing translations, pluralization, and responsive navigation, with regression coverage for locale and rendering behavior.
Build the navbar language link from request.path instead of url_for, so
query parameters can no longer reach route generation: ?host= produced an
off-site absolute link and ?controller=/?action= raised UrlGenerationError
on every page carrying the navbar. Non-GET requests fall back to the root
path, because a form re-rendered after a validation error sits on a route
that only answers to POST/PATCH.

Localize notifications to their recipient at every construction site. The
translations are interpolated at argument position, so wrapping the
Notifications::Create service would not have taken effect; services that
notify several recipients now scope each one separately.

Pin API responses to the default locale, keep I18n.locale in the stats and
insights fragment cache keys, skip persisting a locale preference on
prefetched requests, and localize the insights weekday labels.

Drop 44 locale keys left unreferenced by the rewritten views.
A hovered link is prefetched by Turbo before the user commits to it, and
the session write happened ahead of the prefetch guard, so the language
survived into the next request even though the account preference did not.
Both writes now sit behind the same check, while the prefetched response
is still rendered in the requested language so the preview stays accurate.

Localize the two notification sites the previous pass missed: the Google
Timeline importer, and the recalculation lock-contention warning whose
content is translated before the notification is built. Give the year
count in the recalculation notice a pluralized key instead of splicing an
English "N years" into a German sentence.

Pin the shared-link API to the default locale, as ApiController already is.

Call areas Gebiete in the map settings panel to match the map itself, and
the plan Family-Tarif on the lapsed-family page to match every other
mention.
English stays the interface language until the reader picks something
else. The Accept-Language header no longer takes part in resolving the
locale; it only decides whether there is anything worth offering.

When the browser asks for a language Dawarich speaks, and the reader has
made no choice of their own, a dismissible banner offers the switch. It is
written in the language being offered, since that is the one the reader is
being asked to understand, and it links through the same path helper as
the navbar switcher so the reader stays on the page they were reading.

Dismissal reuses the existing localStorage-backed dismissible controller
and is keyed per language, so a later Spanish or French offer does not
arrive pre-dismissed.

Two locale-selection examples asserted the old automatic switch; they now
assert that English survives a German browser.
Spanish made the two-way toggle untenable: it sent every reader to the
other of exactly two locales, and the English catalogue hardcoded the
German label. The navbar now lists every registered language except the
one being read, and each language is named in its own words, so a reader
can find their own without first understanding the one in front of them.
Each locale file carries its own name, so adding a language touches only
that language's file.

Register :es, translate the keys English gained while German was being
finished, and drop the fragmented keys that were replaced there.
DaisyUI styles the direct child of a menu item, so wrapping the language
links in a shared span swallowed their padding, rounding and hover state
and left them looking unlike every sibling in the navbar. The partial now
emits one list item per language and the three call sites no longer wrap
it, which also gives each link a testid of its own instead of repeating
one identifier across the page.

Name the Spanish legal notice Aviso legal; Imprenta is a printing press.

Default `language_name` to the locale code, since fallbacks are on in
production and a locale that forgets the key would otherwise offer itself
under English's name.
The previous attempt left the duplicate identifiers it set out to remove:
the navbar renders the switcher in three places, so every id appeared
twice on a signed-in page. Each render site now names its own scope, and
the spec asserts the ids are distinct rather than merely present.

That spec also could not see the bug it was written for. Nokogiri repairs
`<li><li>…</li></li>` while parsing, so a caller that wrapped the partial
again produced a tree that looked correct; the nesting is now checked
against the raw response instead.

Fold the guest navbar's languages behind one dropdown, since that menu is
laid out inline and would otherwise grow by an item per language.

Disable fallbacks when reading `language_name`: they are on in production,
where a locale missing the key resolved to the English value and offered
itself as "English" instead of reaching the default.

Call an area an área rather than a zona in the creation dialog, and lower
case the family plan to match its other mentions.
`request.path` is interpolated straight into the href, and a path
beginning with `//` turned the link into a protocol-relative URL that
would carry the reader to another site. Leading slashes are now collapsed
to one, so the link can only ever point back here.

Found while checking a reflected-XSS alert on the same line. The escaping
there holds — the query string is percent-encoded before it reaches the
attribute and the tag builder escapes it again — but the path had this
separate hole next to it.
Language selection moves out of the navbar and into Settings → General,
where it sits with the other account preferences and is saved by the same
Save changes button. Each language is a card carrying its flag and its own
name, chosen with a plain radio group so the picker needs no JavaScript
and stays reachable from the keyboard.

Each locale declares its own flag next to its own name, so adding a
language remains a change to that language's file alone.

The setting is written by the controller rather than left to the locale
around_action: that one saves with `update_all`, which the save at the end
of the update action would overwrite with the settings the request loaded.

Rebuild Tailwind for the utilities the picker needs — the compiled
stylesheet is committed here, and without the rebuild the radios stayed
visible and the selected card lost its outline.

The suggestion banner still offers a language to anyone whose browser asks
for one, and remains the only language control for signed-out readers.
/map now serves the MapLibre map directly and /map/v1 permanently
redirects to /map/v2 (query string preserved). The Settings -> Maps page
(v1-only custom tiles and version toggle) is removed; map settings live
in the map's settings panel.

Monthly statistics maps and publicly shared month maps are rewritten as
standalone MapLibre controllers reusing the v2 style pipeline. Shared
format helpers moved out of the deleted maps/ directory into
maps_maplibre/utils. All Leaflet assets are purged: importmap pins,
vendored JS, CDN links in both layouts, stylesheets, and the npm
dependency.
feat: add complete Spanish locale
Resolves the French locale branch against the German and Spanish work that
landed on dev in the meantime.

The two branches had grown separate locale mechanisms. dev's wins: the
language is chosen in Settings -> General and the browser's language is only
ever offered through a dismissible banner, never applied on the reader's
behalf. So this drops the French branch's LocalesController, PATCH /locale
route, navbar switcher, locale cookies and after_action auto-persistence, and
keeps its translations and string extraction.

French joins the shipped locales: available_locales, language_name/flag, the
suggestion banner copy, and the Settings language cards (now a 2x2 grid so
four languages fill it evenly).

Along the way, resolving the two mechanisms surfaced a few real defects:

- German and Spanish were missing all 86 keys this branch extracts, so those
  readers would have seen English across insights, stats, trips and visits.
  Translated, matching each locale's own terminology (Aufenthalt, not Besuch).
- families.show.family_at_capacity read "Familie ist voll (" in German and
  "Familia a capacidad (" in Spanish -- truncated fragments with a dangling
  paren and no counts.
- full_history_redetect_job's German and Spanish strings interpolated
  %{visits_created}/%{size} while the caller passes visits/months, so the job
  raised I18n::MissingInterpolationArgument for those users.
- Imports::Create and Families::Invite built a notification's body before
  entering I18n.with_locale, giving it a title in the reader's language and a
  body in the request's.
- The yearly digest interpolated the untranslated literal 'Year-End Digest'
  into a translated sentence.
- stale_jobs_recovery_job and create_location_request translated strings just
  outside the locale block for the same reason.
- User#preferred_locale raised on rows whose settings are not a Hash, and
  persist_locale! merged into non-object settings with || (which appends to a
  JSON array rather than setting a key). It also left the in-memory instance
  stale, which Devise then reused for the rest of the session.
- date.order in fr.yml used YAML symbols, which made YAML.safe_load_file fail
  for every locale file and broke the German and Spanish coverage specs.
- Removed locale keys holding Ruby source that string extraction had picked up
  ("width: 20,", "suffix: \" #{@distance_unit}\"") -- translators had begun
  translating them.
Sign-up pinned settings['locale'] to whatever the request happened to run
in — almost always English. suggested_locale treats any stored preference as
"the reader has chosen", so from then on the banner stayed silent and someone
who signed up from a French browser was never offered French again. Only an
explicit choice is recorded now.

Also:
- The French coverage spec loaded its YAML with load_file while the German and
  Spanish ones use safe_load_file(aliases: true). The looser loader is what let
  the date.order symbols through, so it now matches its siblings.
- CHANGELOG: French was missing from the language list; the German/Spanish
  fixes and the navigation breakpoint change were unmentioned.
The sign-up guard tested that a locale param was present, not that it was one
we ship, so `?locale=xx` still stored the English fallback and silenced the
suggestion banner for good. It now asks the same `supported_locale` helper the
rest of the locale chain uses.

Covers the session half of the guard too: choosing a language on the sign-up
page and then submitting the form sends no locale param, and that choice has to
survive.
WITH target AS (
SELECT id, track_id, start_index, end_index, corrected_at
FROM track_segments
WHERE id IN (#{ids.join(',')}) AND start_at IS NULL
Freika and others added 19 commits August 9, 2026 00:42
One detection pipeline (Visits::Detection::Runner) replaces the
stay-point detector, its DBSCAN fallback and the separate area/place
visit writers: dwell sweep -> same-place gap bridging (up to 7 days) ->
transportation-segment reconciliation (confident movement vetoes stays,
boundaries snap to movement edges) -> single merge layer ->
evidence-gated place attribution -> confidence scoring -> overlay-aware
persistence.

Machine visits are regenerable (detection_version stamp); confirmed,
renamed and deleted visits are anchors that survive every re-run. A
same-place tracking silence becomes one continuous visit; a displaced
silence renders as an explicit untracked stretch instead of a fabricated
POI visit. Visits are named after venues only when the evidence supports
it - otherwise they get a street address.

The timeline gates on confidence (medium renders subdued, low collapses
into a per-day disclosure), the untracked-gap threshold drops to 45
minutes, and the stay_max_gap_minutes slider and density-fill toggle are
retired. Fleet re-detection ships as low-priority jobs: enqueued by the
release migration when self-hosted, started from the console on Cloud.
The retired nightly area detector rescanned raw points, which is how a
newly drawn area used to accumulate its history by the next midnight.
Attribution now happens at detection time, so creating or resizing an
area enqueues a relabel pass over the user's existing visit rows instead:
additive only (visits already attributed keep their area), tombstones and
declines untouched, no re-detection and no point scanning.
Bumps the bundler group with 1 update in the / directory: [json](https://github.com/ruby/json).


Updates `json` from 2.20.0 to 2.21.2
- [Release notes](https://github.com/ruby/json/releases)
- [Changelog](https://github.com/ruby/json/blob/master/CHANGES.md)
- [Commits](ruby/json@v2.20.0...v2.21.2)

---
updated-dependencies:
- dependency-name: json
  dependency-version: 2.21.2
  dependency-type: direct:production
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
…dates

Bumps the npm_and_yarn group with 2 updates in the / directory: [postcss](https://github.com/postcss/postcss) and [dompurify](https://github.com/cure53/DOMPurify).


Updates `postcss` from 8.5.15 to 8.5.26
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.15...8.5.26)

Updates `dompurify` from 3.4.11 to 3.4.13
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.4.11...3.4.13)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.26
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Persister: skip anchors that no longer intersect the trimmed interval so
a stay overlapped by several anchors can never re-widen over one; replace
machine rows in bulk (points nullified, join rows removed, one deduped
orphan-cleanup batch, one cache bust per month) instead of per-row
destroy callbacks.

Attaching an area from the map confirms the visit, so the choice
survives re-detection. The 45-minute untracked-gap bar applies only
after a user's history has been re-detected; legacy timelines keep 90.

Confidence backfill batch-loads slim point rows; area relabeling
resolves point-backed centers in one grouped centroid query. A fleet
redetect lock collision re-enqueues with a delay instead of silently
skipping the user, and the release migration survives an unreachable
job backend.

GapBridger returns fragments directly — the untracked-interval output
had no consumer.
Importer-written visits (Google Timeline, Polarsteps) can't be re-derived
from points, so detection now treats them as anchors: excluded from
wholesale replacement and from the manual redetect's pre-delete, with
machine stays trimmed around them.

The manual redetect button clears suggested visits through the same bulk
wipe the Persister uses (extracted as MachineVisitWipe) instead of
per-row destroy callbacks. The fleet rollout staggers per-user jobs 30s
apart, and SKIP_VISITS_FLEET_REDETECT skips the migration enqueue for
admins who want to run it by hand.

Confidence now reflects what actually persists: stays trimmed around
anchors and visits stitched across batch edges are rescored from the
surviving interval (shared StayScoring / VisitRescore, which the legacy
backfill reuses). Editing a suggested visit through the API confirms it,
matching the web controller, so place/area choices from the map panel
survive re-detection.

visits_redetected_at predates v3, so a release migration clears it —
the tighter 45-minute untracked-gap bar waits for a real v3 redetect.
Place attribution skips the reverse geocoder whenever point geodata
already names the stay.
Days per Country was buried in the map's Create/tools tab, which is an odd
home for a yearly report: it sits next to visit and area creation tools, and
nothing about "Create" suggests a day counter. Insights is where the rest of
the per-year analytics live, so the entry point moves there, next to the year
selector.

- Move the partial from map/maplibre/_residency_modal to
  shared/_days_per_country, since it is no longer map-specific, and fold the
  trigger button into it so the trigger and the modal stay together.
- Trigger now uses data-action instead of an inline onclick dispatching a
  custom document event; the now-unused document listener is removed from the
  Stimulus controller.
- Move the i18n keys to shared.days_per_country across en, de, es and fr,
  preserving the existing translations.

The turbo frame still loads from map/residency; moving that route and
controller is left out of this change to keep the diff reviewable.
A debounced re-run that would recreate exactly the machine rows already
standing is now a no-op: the Persister compares the regenerated stays
against the window's rows and skips the delete+insert, so ids, point
claims and the daily bulk re-scan stop churning points.visit_id.

The "new visits suggested" notification (and per-place geocoding
enqueues) fire only for visits that don't overlap any machine row that
stood before the run — an ongoing stay growing every few minutes is not
news.

Full-history redetection wipes machine rows whose backing points are
gone before stamping success, so stale visits can't outlive the points
that produced them. Rescoring skips point-less visits entirely — an
unscored row renders at full strength instead of a fabricated low band.
Relabeling an area now also names suggested machine visits after it,
matching what detection does for area-attributed stays.
Visit.machine_detected is now the single scope for rows detection may
regenerate: suggested, not imported, not demo-seeded, and not annotated
with a note. Everything outside it anchors re-detection, so demo data
and visits the user wrote notes on survive every run with their notes
attached. The bulk wipe also collects suggested-place join ids so
photon places referenced only through them get their orphan cleanup.

Boundary snapping now drops a stay's fixes that fall outside the
snapped interval — they are movement, not dwell, and no longer shape
the center or get claimed. The no-change fast-path additionally
compares claimed-point counts, so a late-arriving point inside an
otherwise identical stay still forces regeneration and gets claimed.

The fleet redetect worker respects the visit-suggestions opt-out and
stamps visits_redetected_at only after a clean run, so a partial
redetect doesn't unlock the tighter timeline gap bar.
StayAssembler finalizes before applying the dwell and point floors, so
a boundary-snapped stay is judged on the fixes that actually remain in
its interval; the Persister applies the same point floor to stays it
trims around anchors.

Confidence gating on the timeline activates only after the history has
been re-detected — legacy rows carry scores from the retired scorer,
and those must not subdue or collapse visits on upgrade day. A month
the CandidateLoader refuses over its point cap now counts as failed,
so partial redetects report honestly and never stamp
visits_redetected_at.
Reworks the previous commit's entry point. Rather than a button on Insights
that opens a modal, the report is now part of the page, laid out like the
Activity Overview and Activity Streak row above it: countries with their
collapsible stay periods on the left, the year heatmap on the right.

- The report takes its year from the page's year selector instead of carrying
  its own dropdown, so the two can no longer disagree.
- Hidden for All Time, which has no calendar year to count, matching how the
  heatmap and streak cards already behave.
- Loaded in a lazy turbo frame, since counting a year of points is a grouped
  query over the points table and it sits below the fold.
- The modal partial and the residency Stimulus controller are gone: with no
  modal to open or close and no year select to react to, neither had any work
  left to do.
- Card copy moves to the map.residency.show scope across en, de, es and fr,
  keeping the existing "Not tax advice" line as the heatmap card's subtitle.
…n path

New accounts default visits_redetected_at to their creation time — they
only ever have v3 output, so confidence gating and the tighter gap bar
apply from day one instead of never activating for post-rollout
signups. Upgraded accounts still start cleared and unlock on redetect.

Anchor trimming is now exact interval subtraction: an anchor strictly
inside a stay yields both flanks as separate visits instead of silently
discarding the shorter one. Batch-edge stitching refuses to bridge a
silence that holds a point-less anchor (imported and manual visits own
no points), closing the last way a machine visit could overlap one.

The re-detect button no longer wipes everything up front: windows are
replaced as they regenerate, so a failed or capped month keeps its
existing rows instead of losing them, and orphan-place cleanup rides
the Persister's batched path. Area relabeling busts the month summary
cache for the days it touches.
Move Days per Country from the map to Insights
MachineVisitWipe.flush_side_effects is the single place wiped rows'
side effects run — orphan-place cleanup (direct and suggested-place
joins) plus month cache busts — so the out-of-range purge no longer
leaks photon places the way the removed upfront wipe used to cover.

The re-detect button stamps visits_redetected_at only after a clean
run, matching the fleet worker: a partial redetect neither unlocks the
tighter gap bar and confidence gating nor starts the retry cooldown.
Boundary snapping moved a stay's end before the fixes reacquired on the
way out of a GPS-dark venue, and the post-snap point filter then
dropped the stay below the point floor — a planetarium visit with one
arrival fix, a 78-minute dark-dome silence and four departure fixes
vanished entirely (about 1,100 such stays across one real history).

A fragment's fixes are same-place by construction (DwellSweep clusters,
GapBridger and chain_merge only join same-place fragments), so the
snapped interval is a statement about time, not about evidence: keep
every fix for the floors, the center and the claim. Movement pollution
stays handled where it belongs, in the reconciler's veto.
New labeled scenario: one fix on arrival, 78 minutes of GPS-dark
silence, four fixes reacquired with the departure walk already under
way — the shape whose evidence sits entirely at the snapped-off edges.
The eval gate requires one stay of at least 70 minutes, so no future
cleanup anywhere in the pipeline can silently drop it again; verified
red against the reverted assembler filter, green against the fix.
Visit detection rewrite: bridged stays, movement reconciliation, honest gaps
.where(timestamp: start_at..end_at)
.where.not(lonlat: nil)
.where('anomaly IS NULL OR anomaly = FALSE')
.where("NOT #{Points::NullIsland.sql_predicate}")
Freika and others added 6 commits August 9, 2026 23:50
…aad51

chore(deps): bump json from 2.20.0 to 2.21.2 in the bundler group across 1 directory
…yarn-116291eeda

chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Age spec users out of the cooldown the born-redetected default now
starts at creation, add dark_venue to the documented scenario list so
the shared invariants cover it, and give the eval baseline JSON its
trailing newline for biome.
@Freika
Freika merged commit 246e280 into master Aug 11, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants