# Group record

The logged group itself: its id and times, the session and profile version it belongs to, its soft delete, its free-text notes, and which page describes each other part of it.

## Generated table

**Record:** observation · **Covers:** `id`, `created_at`, `updated_at`, `deleted`, `owner_id`, `session_id`, `profile_id`, `profile_version`, `notes`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `created_at` | string | yes | - | no | as stored |
| `deleted` | boolean | yes | - | no | as stored |
| `id` | string | yes | - | no | set by the server for each share |
| `notes` | string | yes | - | no | emptied when notes are hidden; the connector's old value block cut when location is hidden |
| `owner_id` | string or null | yes | - | no | set by the server for each share |
| `profile_id` | string | yes | - | no | set by the server for each share |
| `profile_version` | number | yes | - | no | as stored |
| `session_id` | string | yes | - | no | set by the server for each share |
| `updated_at` | string | yes | - | no | as stored |

## What it holds

One logged group is one record. These fields are its frame: who owns it, when it was made, which session and which version of which profile it was shot with, whether it is deleted, and a line of notes. The rest is on sibling pages: [group](group.md) (dial, hold, centre, size, count, calm), [target](target.md) (range, angles, wind), [air](air.md), [muzzle velocity](muzzle-velocity.md), [prediction](prediction.md), [positions](positions.md), [photo](photo.md) and [target marks](marks.md). The session is on [session](session.md).

## What to know first

**A group keeps the profile version it was shot with.** `profile_version` is set when the group is logged and the app never changes it. A fit reads the group's dial with that version's elevation tracking factor. The profile's other numbers come from the version being fitted.

**A fit leaves a group out if its version is not a live version of the profile, or differs in bullet, twist, sight height, or the zero's range, air or slope.** Nine values are compared. Ballistic coefficient, muzzle velocity, scope factors, zero height and zero offset are not.

**A group is changed in place, not versioned.** The app can edit it, delete it (a soft delete) or remove its photo; the connector cannot change a saved group at all. A delete wins on every device.

**Ids and times come from the device, the owner from the server.** The account that pushed a group becomes its `owner_id`. Sync never compares `updated_at`.

## The fields

**`id`** The group's own id, made on the device that logs it (a time-ordered random id). Required. It is the key under which sync stores the group, so pushing the same id again replaces the stored group. A saved calibration names its groups by this id. A shared log shows a different id for it (see "Sharing and privacy"). Example: 01a2b3c4-0000-7000-8000-000000000001.

**`created_at`** and **`updated_at`** ISO times in UTC, read from the clock of the device that made or last changed the group. Both are set to one instant when the group is made. They are the time of logging, not of the shot. `created_at` orders groups on every screen. `updated_at` changes only when the group is edited, deleted or its photo is removed. Example: 2026-10-14T15:20:31.402Z.

**`deleted`** True once the group is soft deleted, false otherwise. Always present. The group stays stored as a tombstone; no reader uses it after that (see "Changing it").

**`owner_id`** The account that owns the group. Null on a device that is not signed in. The app writes the signed-in account's id when it saves a group, and the server overwrites it with the pushing account on every push, whatever the group said. A shared log shows null.

**`session_id`** The session the group was logged in. Required. The app logs into the session screen it is on. `log_group` uses the session you name, else the active log (the rule is on the [session](session.md) page), else it makes a session called "Logged by voice" and pushes it with the group. The Calculator's "Log this shot" opens the active log's form the same way, or starts a session. A group never moves to another session.

**`profile_id`** and **`profile_version`** Which profile, and which saved version of it, the group was shot with. Both required. The app takes the highest version of the profile chosen on the Session screen (a profile whose highest version is deleted is not offered; see [versions](../profile/versions.md)), as it was when that screen was opened. `log_group` takes the newest version the server holds, for the profile you name, else the phone's, else your only one. On an account with several profiles, a call that names none asks "Which profile?" when the phone's report is 30 minutes old or more, or of unknown age (since 2026-10-15). It does not use the version the phone shows if that is older, and its reply does not say so. When you named no profile and have several, its spoken line begins with the name of the profile it used. The version is a whole number from 1. Example: a profile id of "my-rifle-v1" and a version of 3.

**`notes`** One line of free text, required (empty is fine). The app saves it as typed, and adds "air: weather model" to the notes of a group logged with weather-model air. The Session screen reads that mark in the last group's notes to start the next group with the same choice. `log_group` saves "[voice]", then the spoken notes, then where each value came from, by word and never with its number ("[from the phone: temperature, pressure]", "[default: humidity, latitude]"; since 2026-10-15), then "[MV not measured]" and "[SD/ES/n not given]" when they apply, then "[phone data N min old]" when the phone's data was over 30 minutes old. Groups logged before 2026-10-15 carry an "[assumed: ...]" list with the values instead. Nothing else reads the notes as data; the CSV export carries them as text, and so does the admins' pool when the share shows notes.

**Limits, by writer.** The profile limits table does not apply: a group is not checked by the profile save. These come from running each writer.

- App, the Session screen: a profile must be chosen ("Pick a profile"). The notes box has no limit; a 5,000-character note saved whole.
- Connector, `log_group`: `notes` at most 500 characters (501 is refused and nothing is logged), but the saved notes are longer, because the prefix and the lists of where values came from are added (the length depends on how many values came from where). A phone field that is not a number is named, never quoted (since 2026-10-15), so the addition is bounded. `profile_id` must be one of yours or the group is not logged ("Which profile?" and your list). `session_id` must be a live session of yours ("No session"). The version is not an input.
- Sync: `id`, `session_id` and `profile_id` must be text, `notes` text, `deleted` true or false, `owner_id` text or null. The schema accepts a `profile_version` of 0 or 1.5; the push then refuses anything that is not a whole number from 1. Sync does not check that the profile, the version or the session exists.
- Fitter: a group whose profile id is another profile's, or that is deleted, is skipped without a word; one whose version is missing is listed with a reason.

## Who writes it

**The app.** "Add group" on the Session screen builds the record with `buildObservation`; "Log this shot" on the Calculator opens that form with the shot copied in and the same profile chosen, and with a note when the range, the air or both are still what a connected rangefinder gave (see [target](target.md) and [air](air.md)). The profile list is read once when the screen opens, so a profile saved on another device while it stays open is not picked up until it is reopened. The app can also edit a saved group, delete it (a confirmation first), remove its photo, and mark its holes on a target photo (see "Changing it" and [target marks](marks.md)).

**The connector.** `log_group` builds the record with the same function and pushes it. It has no tool to edit or delete a group. It writes through the connector link's `log` scope.

**Other paths.** A backup restored in the app writes each group as the file held it; the file is refused if a group names a profile version that the file does not hold. Sync copies a group between a person's devices unchanged. The server forces `owner_id` and can renumber `profile_version` (see "Changing it"). The fitter writes nothing here. For the delete of a profile and its restore, see [versions](../profile/versions.md) and [origin](../profile/origin.md).

## Who reads it

The solver does not read these fields. They say where a group belongs.

**The fitter.** `toCalGroups` keeps a group only if it is not deleted and has the profile id being fitted. It then finds the group's version among the profile's versions: a live one, or a deleted one that the group names (the app's fits since 2026-10-24; the connector still reads only live versions). If there is none, the group is left out with "profile version N is not on this device". Otherwise `ineligibleReason` compares that version with the one being fitted, and the group is left out with the first difference it finds, worded "shot with v2: different twist (10 vs 9)" (the group's value first). The nine values, in the order they are checked:

| Compared | Where in the profile |
|---|---|
| bullet diameter, weight, length | `ammo.bullet.diameter_in`, `ammo.bullet.weight_gr`, `ammo.bullet.length_in` |
| twist, twist direction | `rifle.twist_in`, `rifle.twist_dir` |
| sight height | `rifle.sight_height_in` |
| zero range | `rifle.zero_range_yd` |
| zero air | `rifle.zero_env` (its numbers, the zero altitude included when there is one; absent counts as "none") |
| zero slope | `rifle.zero_incl_deg` (absent counts as 0) |

This was run for this page with a made-up profile: a change of any of the nine left the group out; a change of the muzzle velocity, the ballistic coefficient, the scope factors, the bullet's name, the zero height or the zero offset did not. A group that passes is solved again with the version being fitted and the elevation tracking factor of its own version (see [group](group.md)).

The comparison is exact, with no tolerance: a bullet length of 1.2400001 against 1.24 leaves the group out.

Since 2026-10-17 a group that passes is still left out when it was shot before the latest turret move recorded on one of the profile's [zero checks](zero-check.md), worded "shot before the turret was moved on 2026-10-16 (a different zero)".

The velocity-versus-temperature fit does not use `toCalGroups`. It takes every live group of the profile that has an ammo temperature, from any version.

**The app.** The Session screen lists the session's groups in `created_at` order. The Calibrate screen lists the groups a fit left out, each with its reason. The CSV export writes `profile_version`, `session_id` and the group id, and looks up the profile name by id and version, leaving it blank if the version is gone.

**The connector.** `get_session` returns a session's live groups as stored, with a marked group's results in place of its marks and, on an MOA scope, each angle also in MOA (since 2026-10-16). `calibrate_profile` lists the groups it left out with the reason and refuses a save when the groups changed since the preview.

**The server.** The shared views and the admins' pool take live groups of the shared profile whose session is live.

## Why it is stored

A group is only worth a fit if it can be solved again exactly as it was shot. The profile id and version say with what numbers it was shot, and a profile version never changes once saved, so a later fit can find them. The session ties the groups of one day together, which is how a fit weights a day's muzzle velocity. The soft delete exists so that a delete reaches the other devices by sync. The times let you sort and see when a group was logged. The notes hold what no field has room for, and the connector uses them to say what it assumed.

## Changing it

**A group is not versioned.** It is one record, changed in place. The connector changes nothing on a saved group. The only changes to one are:

| Change | Writer | `updated_at` |
|---|---|---|
| Edit the group (since 2026-10-13) | the Session screen | set to now |
| Delete the group (`deleted` true) | the Session screen | set to now |
| Remove its photo | the Session screen | set to now |
| Mark its holes on a target photo, or remove the marks (since 2026-10-16) | the Session screen | set to now |
| `profile_version` moved to a new number | the server and the device that pushed it, when two devices saved the same profile version number | not changed |

To correct a mistake, edit the group on the Session screen (since 2026-10-13): the button on its row offers Edit and Delete, Edit opens the group in the form, and "Save changes" writes the same record, with the same `id`, `created_at`, `session_id`, `profile_id` and `profile_version`. What an edit keeps and works out again is on the [group](group.md) page. A group cannot be edited on a device that does not hold its profile version. There is no copy of a group and no restore of a deleted one: copying a shared profile copies the profile only, and a deleted group stays deleted. A delete is a soft delete: the record stays, marked deleted. After it the fitter, the Session screen, the CSV export, `get_session` and shared views all skip the group. A backup file keeps it.

**Two devices.** The server keeps the whole group from the latest push that reaches it. It does not compare `updated_at`, and it does not merge fields. One rule is firm: a group stored as deleted cannot be brought back. If a device later pushes the same group as not deleted, the server answers that it is stale, and the device takes the deleted copy. On a device, a pulled copy replaces the local group unless the local group has a change not yet sent; then the local one is kept and sent, and replaces the server's. Order of arrival decides, not time.

**A saved calibration is not touched by a delete.** It keeps the ids of the groups it used, in `calibration.used`. A later fit does not see the deleted group. A connector save made from a preview is refused if a group the fit could use was deleted, lost its photo or was added in between: the preview's fingerprint covers the id, `updated_at` and role of each such group (for velocity against temperature, each group with an ammo temperature). A change to a group the fit had left out does not alter it. See [group](group.md).

**Which profile version a group has, and what happens to it.**

- The version is final once the group is logged. A new profile version never moves old groups to it.
- A fit of version 4 uses a group of version 2 when the nine values above match, with the elevation tracking factor of the group's own version (windage is not treated this way: see [group](group.md)). A group with wind by stretch is fitted at the best-guess flight of its stretches, for the same elevation: see [target](target.md). Edit the bullet's diameter, weight or length, the twist or its direction, the sight height, or the zero's range, air or slope, and the old groups drop out of the next fit; the profile screen's keep-or-reset choice (see [versions](../profile/versions.md)) does not change that.
- When two devices saved the same profile version number, the server keeps one and renumbers the other. It moves the groups that came in the same push; the device moves the groups it pushed and those it has not sent yet. A device that pulls the other version before it has pushed its own does the renumbering itself. `updated_at` does not change. This is the only way `profile_version` changes after logging.
- If the profile is deleted, every one of its versions is deleted with it and its shares stop. Its groups stay, but no fit can read them while the profile is gone.
- If the profile is restored, the restore saves the last version's content as a new live version, and the older versions stay deleted. Since 2026-10-24 your own fits in the app, and a reader's fit of a shared log, read the deleted versions a live group names, as the admins' pooled fit does, so the older groups stay in. Since 2026-10-25 `calibrate_profile` and `next_target` do the same: the server hands the connector the deleted versions its groups name. Stopped shares do not reopen. A new share of the restored profile still shows the deleted versions that a live group used. Checked for this page on a made-up profile.
- On a device that has not yet pulled a version, its groups are left out the same way until the pull arrives.

## Sharing and privacy

A shared log shows each of these fields as the owner has them, except as listed here.

- `id`, `session_id` and `profile_id` become other ids, the same for one share on every load and different on every other share, and the same for every kind of reader (one person, the public, the admins). The links hold: a group's `session_id` and `profile_id` are those of the view's own session and profile versions. These three fields never carry the owner's real ids, so a reader cannot match a group on two shares by these ids. The times and the numbers are the same on both, so a reader who has both shares can still match the groups.
- `owner_id` is null. The owner's name is shown by the share, not by the group.
- `notes` is empty when the share hides notes, which a public share does by default. With notes shown and location hidden, the old "[assumed: ...]" block of a group logged before 2026-10-15 is cut, since it can hold the phone's exact pressure and bearing (owner decision D7, 2026-10-15); the shooter's own words stay. With notes hidden the reader loses the "[voice]", the source lists and "air: weather model" marks too, so they cannot tell a weather-model air from a typed one. The rangefinder marks are not in the notes but in the `target` and `environment` blocks, and travel with them: a reader sees which groups had a rangefinder's range or air, with notes hidden or not. The chronograph mark is in the `mv` block: that the shots came from a chronograph is shown with the shots, the name and time of the string only with notes shown, and the string's id to no reader (the [muzzle velocity](muzzle-velocity.md) page).
- A deleted group is not in the view. A group in a deleted session is not either.

What a reader can work out: `created_at` shows the time of day each group was logged, and so how the day went. `profile_version` with the shared profile versions shows exactly which numbers the group was shot with. `updated_at` later than `created_at` means the group was changed after it was logged (for example its photo was removed).

---

[All reference pages](../index.md)
