• v1.0.0-Beta.04 6a39713981

    v1.0.0-Beta.04
    All checks were successful
    CI / check (push) Successful in 1m30s
    Release / release (push) Successful in 2m11s
    Docker Release / build-and-push (push) Successful in 4m19s
    CI / tests (push) Successful in 5m7s
    Pre-release

    k4wit0 released this 2026-09-01 15:36:11 +02:00 | 68 commits to main since this release

    Photographs, real watch files, and updating with people training

    Added

    • Photographs have a screen of their own at /body/photos: a grid of
      thumbnails grouped by the month they were taken, newest first, one tap to see
      one large, and swiping across it to move through them. Adding is two buttons
      of ours — take one, or choose one — rather than the browser's grey "Choose
      file / No file chosen", which is a web page announcing itself on a phone.
      Comparing is a mode entered from the header, and it appears only when there
      are two photographs to compare.

    • A third answer to a synchronisation batch: deferred. It means this server
      cannot take the change yet, and a later one may.

      There were two, and they were answers to different questions. "This identifier
      belongs to another account" is never going to become true; "this server has
      never heard of that exercise" is a statement about a release, and the sentence
      after it is usually the administrator updating the server. Both were answered
      with "refused, and will never be accepted", and the client's correct response
      to that is to throw the row away. Measured: four recorded sets refused by the
      server and discarded by the phone in the same second, existing nowhere
      afterwards.

      Deferred work is held on the device, out of the send queue so it is not
      offered every minute, and offered again the first time the phone meets a
      server running a different build. The server now tells clients what build it
      is, for exactly that purpose. Nobody has to be told their session was waiting.

    • The connection detail says how much is held and what it is waiting for, and
      signing out counts it. Held training is unsent training, and that button is
      the last thing between it and a browser somebody else uses.

    Changed

    • The body screen ends in a row that says how many photographs there are and
      opens them, instead of carrying the whole thing. It was 865 pixels of form on
      a screen somebody opens to read a number.

    • An update no longer refuses to run while somebody has a session open. It
      reports who is training and carries on.

      The refusal read well and was backwards. Restarting the server mid-set is not
      the failure the rest of this application was designed against — it is the case
      the rest of it was designed for: a set is written to the phone's own store
      before it appears on screen, a failed request leaves the queue exactly as it
      was, and the phone offers it again by itself. From the gym floor a server
      being updated and a corridor with no signal are the same event. What the
      refusal achieved was letting one person who forgot to close a session block
      every update on the server, and it did not even cover the case that matters —
      somebody who finished half an hour ago with their phone in a locker and
      nothing sent yet. lastgym update --when-idle keeps the old behaviour for an
      administrator who would rather wait, or for a nightly cron.

    Fixed

    • A .fit file from a real watch imports instead of answering 500.

      Two things, and neither showed against the files these tests write for
      themselves. The reader took total_elapsed_time for hundredths of a second
      where the profile scales it by 1000, so every activity came out ten times as
      long as it was — a three-and-a-half hour walk filed as thirty-four hours,
      next to its own twelve thousand samples whose last offset said three and a
      half. And the watch reports moving time 170 milliseconds longer than elapsed
      time, out of those three and a half hours, which the table forbids and SQLite
      refused; from outside it was "an error, and nothing happens".

      A session is now brought inside what the table allows before it is written:
      a number out of range by rounding is clamped, and one that cannot mean
      anything is dropped. internal/core/activity/testdata/walk.fit is a real
      Garmin file, cut down — every definition message as the watch wrote it,
      including the six times it redefines a record as sensors come online — with
      every position zeroed and every timestamp moved off the day it happened.

    • One photograph is shown once. The old screen was a permanent
      before-and-after, so with a single photograph both sides chose the same one
      and it appeared twice, next to itself — which reads as the upload having gone
      wrong. It had not: the file was fine, and its orientation was fine too.

    • The sheet closes when a photograph is added, and the screen behind it
      redraws. It used to empty the file field and stay open, which reads as
      nothing having happened.

    • The local database is opened without naming a version, so an older bundle can
      no longer refuse to start against a newer database.

      A progressive web application serves whatever its service worker has, which is
      not always the newest build. Opening at a fixed version meant that after a
      release which raised it, a tab holding the previous bundle asked for version 1
      against a version 2 database — and IndexedDB refuses to go backwards. The
      start-up screen showed "LastGYM could not start" over the browser's own
      sentence, in English, and the obvious remedy — clearing site data — is the one
      action that would have destroyed the queue. Nothing was ever lost, but the
      application would not open.

      There is no version constant any more. What decides an upgrade is whether the
      stores this build needs are there, and the only version ever requested is one
      higher than what is on disk. An older build meeting a newer database now
      simply works: upgrades only ever add stores, so the extra ones sit unused.

    • Signing in now does everything booting into a session does. It did not, and
      the consequences were only visible in a browser.

      Boot claimed the local store for the account and started synchronising, but
      only when somebody was already signed in — and the person signing in had, by
      definition, not been. So for the whole of the session they had just started:
      no synchronisation, no online listener, no heartbeat, and no flush when the
      application closed, which the code that registers it calls "the last chance".
      Nothing was lost, because none of it is discarded, but nothing reached the
      server until the application was reopened.

      Worse, the store was never claimed for the account that had just arrived.
      Measured in a browser: signed in as a second person on the same device, the
      local database still belonged to the first, with their rows and their sequence
      number in it. Nothing of theirs was on screen — the screens that could show it
      read from the server — but their queue was there, and a queue is sent under
      whoever is signed in now.

    • A parent that has not arrived and a parent that belongs to somebody else were
      the same refusal. Both answered "missing parent", which was invisible while
      every refusal was permanent and stopped being invisible the moment a missing
      parent became something worth waiting for: a set aimed at a stranger's session
      would have been offered again after every update, for ever.

    • The installer no longer prints the updater's output on top of its own. An
      upgrade hands the release to lastgym update, which is where the five
      refusals live, and that command talks to whoever ran it — announcing its own
      fetch, its own signature check and a column of steps, then writing the reason
      it stopped twice. All of it landed in the middle of the installer's lines. It
      is captured now and reported in the installer's own voice: one line for the
      signature, one for the backup when one was really taken, and one for the
      reason when it refuses.

    • An upgrade downloads the binary once instead of twice. The installer fetched
      it and then the updater fetched it again behind its own signature check —
      fifteen megabytes, and the word "fetching" printed twice, which reads like
      the script lost its place.

    • The summary at the top of an install shows the address somebody can open
      rather than the one the process binds to. It binds to every interface on
      purpose, but 0.0.0.0:8080 is an implementation detail printed to somebody
      who wants a link.

    Downloads