-
v1.0.0-Beta.06
Pre-releasereleased this
2026-09-01 18:56:33 +02:00 | 56 commits to main since this releaseThe status bar on iOS, and an installer that reads like the others
Fixed
-
On an iPhone the application no longer draws under the status bar. With
viewport-fit=coverand a translucent status bar, iOS hands the whole
display to the page — and the top of it was never given back, so the greeting
and the connection dot sat behind the clock and the battery. The bottom inset
had been handled in four places from the start; the top in none, and there is
nothing to notice on a device without a notch, which is why the same build
looked right on Android.The strip is reserved on the shell rather than on the scroll container, and
that distinction is the fix: a scroller's own padding scrolls with what is
inside it, so content would have slid up through the strip and passed behind
the clock in full view — the same trap that once put a band above every
sticky header.
Changed
-
The installer reads like Lastboard's, because it is the same installer.
Same greeting, same steps in the same order and the same words, the same
"Installation complete!" and the same closing line with the address. What was
there before was a different script doing the same job in a different voice —
lowercase steps with no ellipsis, a block of five facts nobody asked for, and
an ending that named a commit hash.Three things stay different, and each because the products differ. A port in
use is reported rather than freed: Lastboard kills whatever holds it, and on a
machine where that is somebody's other service it is an outage caused by an
installer. The signature is checked, and says so, because these releases are
signed and Lastboard's are not. And an upgrade keeps the configuration file
untouched and says so, rather than rewriting it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v1.0.0-Beta.05
Pre-releasereleased this
2026-09-01 18:07:30 +02:00 | 62 commits to main since this releaseThe phone: icon, zones, sessions that end and imports that speak
Fixed
- The installed application wears the mascot on a phone. The manifest declared
one icon and it was an SVG; Chrome on Android will not take a vector for a
home-screen icon, and with nothing it could use it kept whatever it already
had — the dumbbell that came before the mascot. The same drawing now ships as
PNG at 192 and 512, with a square-edged 512 formaskableso the platform's
own mask has a full plate to cut from, and anapple-touch-iconbecause iOS
never reads the manifest for that one. - The icon is one named face rather than whichever one was on screen.
scripts/icon.tstook the firstsvg.mascotit found, and which of the ten
moods that is depends on the account that happened to be open — so the
identity of the application was decided by somebody's training week, and a
rebuild could quietly change the face on every home screen. It signs out
first, asks formeetingby name, and refuses to write anything if that face
is not there. - The icon's plate is the canvas colour. It was left on
#121212when the
canvas moved, so it sat a shade lighter than the screen it opens. - Finishing a session ends it.
finish()recorded the workout and then left it
in the signal, and the Train tab asks that signal before it asks the device —
so touching Train after finishing walked straight back into the workout that
had just ended, every exercise still there, and the only way out was the menu
again.clear()existed for exactly this and nothing had ever called it. - A session says when it is over. Every exercise asks "done, or one more?" when
its sets are in; the last one was answered and the screen then sat there with
nothing owing, nothing to do and nothing saying so. The way out was behind
the three dots, which is where somebody goes to abandon a session rather than
to complete one. - Importing an activity says it worked. The message was written into the notice
and thenonChangedredrew the screen, throwing away the node it had just
been written to — so a successful import said nothing at all, and only the
new session appearing behind the sheet suggested anything had happened. It is
a toast now, which lives outside what is being redrawn. Errors stay in place:
a failure is to read and act on, not to glance at before it slides away. - The upload sheet closes when the file is in, and its file control is a button
of ours rather than the browser's grey "Choose file / No file chosen" under a
label repeating the title of the sheet it was in. - Time in zones is laid out.
.zone-barswas a class with no rule anywhere in
the stylesheet, so it rendered as the browser's own list — discs down the
margin, and three inline spans with nothing between them, which ran the
zone's range and the time spent in it together as one number: "Zona 1
114–1265:55". Nothing looks wrong reading the stylesheet, because the rule
that would be wrong is not there. Each zone is now its name, its range and
its time on one line with the bar underneath at full width, and a zone with
no time in it draws an empty track rather than a stub that says "a little". - A watch file can be chosen on a phone. Chrome on Android turns the file
input'sacceptlist into an intent filter, and it can only use the entries
it can map to a MIME type —.fitand.tcxare not in Android's table, so
what reached the picker was "XML" and a watch file was not something that
could be picked. The same screen worked from a computer, which is how a
filter that exists on one platform only manages to hide.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The installed application wears the mascot on a phone. The manifest declared
-
v1.0.0-Beta.04
Pre-releasereleased this
2026-09-01 15:36:11 +02:00 | 68 commits to main since this releasePhotographs, 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-idlekeeps the old behaviour for an
administrator who would rather wait, or for a nightly cron.
Fixed
-
A
.fitfile from a real watch imports instead of answering 500.Two things, and neither showed against the files these tests write for
themselves. The reader tooktotal_elapsed_timefor 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.fitis 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, noonlinelistener, 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 tolastgym 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, but0.0.0.0:8080is an implementation detail printed to somebody
who wants a link.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v1.0.0-Beta.03
Pre-releasereleased this
2026-09-01 12:19:13 +02:00 | 76 commits to main since this releaseNative status bar and a cleaner hero
Added
- The mascot is the project's icon. The browser tab, the installed app's icon,
the logo on the README and the face on the sign-in and setup screens are now
one drawing rather than four things that could drift. scripts/icon.ts, which builds that icon out of the running application
rather than from a second copy of the same curves.- A
.dockerignore. Without one the whole working tree went to the daemon on
every build — 286 MB down to 4.2 MB, measured, and more to the point a stray
database in the tree is somebody's training and has no business inside an
image that gets pushed to a registry. - A container image, built and pushed on every tag.
:betawhile there is no
stable release;latestis only ever moved by a stable one, deliberately, so
nobody pulling it lands on a prerelease. It is built without a signing key, so
the binary inside refuses to update itself — updating a container is pulling a
new image. scripts/screenshots.ts, and the screenshots it takes. Never by hand: a
screenshot taken by hand is whatever the window happened to be that afternoon.
It refuses to run if the server is not answering, because otherwise it
photographs the offline screen out of the service worker's cache and nothing
fails..env.example, for the container path. Every variable in it is one the server
actually reads, checked against the config package.- A test that everything the binary redistributes is named in NOTICE. Apache-2.0
requires the attribution, and a NOTICE is exactly the kind of file that stops
being true when a dependency arrives through another dependency.
Changed
-
The README, CONTRIBUTING and SECURITY files follow the same shape as the
other CodigoSH projects: the centred header with the logo, the badges and the
screenshots; "How Development Works" and "Join the Team"; and a security
policy that leads with how updates are delivered and ends with how to report
something. The substance is this project's own — it signs its releases and
says what its threat model does not cover — but the shape is the house's. -
The README carries the house blocks verbatim: Support & Recognition with its
three parts, and the signature line every CodigoSH repository ends with. Those
are the organisation's, not this project's, and they now diff clean against
Lastboard's. Its sections and badges line up one for one, and the picture at
the top is a composed hero — the name and a headline down the left, the
application on the right with rounded corners and a shadow — built by
scripts/mockup.shfrom the screenshots, at the same 3200×1800 as the other
projects. -
The README is a third shorter and stops repeating the documentation. What it
said about the first-run token was two versions out of date — the token was
removed — and what it said about the languages was one: all twenty are
translated, and what fourteen of them are missing is the plural forms. -
Screenshots are taken in English and dark, from an account seeded for the
purpose and deleted afterwards. Pointed at an account seeded weeks earlier the
home screen showed a line falling to zero and the calendar an empty month:
both true, both a poor picture of what the application does. -
deploy/compose.yamlisdocker-compose.ymlat the root, where the other
projects keep theirs. -
Releases are cut with CodigoSH's release script — Lastboard's, the same file
in every project, with a CONFIGURATION block at the top for the handful of
things that genuinely differ: which files carry the version, what the
preflight runs, and where the notes come from. Everything below that block is
byte-for-byte the same, so a fix made in one project is a file copied to the
others. -
scripts/release.sh beta "the headline"checks the tree is clean and level
with the remote, runs the whole suite before anything is written, works out
the next version, stamps it, writes the notes, tags and pushes — the tag
last, because the tag is what CI builds from.--dry-runprints every
decision and writes nothing. -
The two halves of a release are two scripts.
scripts/release.shis the one
a person runs;scripts/build-artefacts.shis the one CI runs, and it is
where the signing key is needed — a release cut from a laptop should not need
one on that laptop. -
The version lives in
package.jsonand is stamped there by the release
script, the way it is in Lastboard.
Fixed
- The status bar of the installed application is the colour of the canvas
instead of the accent. On a phone it was painting an orange band across the
top of a dark screen, which is a browser announcing itself in the one place
an installed application is supposed to look like it owns the whole screen.
It follows light and dark on its own with no script, and a theme chosen by
hand beats the one the phone is set to. - Scrollbars are gone on a touch screen. They were a desktop affordance
inherited whole, and no application on a phone draws one. - The screenshots are taken with touch emulation on. Setting a phone-sized
viewport does not change the input device, so the pictures were still being
rendered as though a mouse were pointing at them — the scrollbar came back in
them while being correctly absent on a real phone. The script now refuses to
run if the page reports a fine pointer. - The two phones on the README's hero image are placed by their screen rather
than by their shadow. The blur spills out on every side, so compositing the
file at a coordinate put the shadow there and the phone somewhere else, which
is what pushed the second phone against the edge of the picture and made it
look cropped. They have a bezel now, and the feature pills are spaced from
their own measured widths instead of from three numbers written by hand.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The mascot is the project's icon. The browser tab, the installed app's icon,
-
v1.0.0-Beta.02
Pre-releasereleased this
2026-09-01 05:31:14 +02:00 | 89 commits to main since this releaseLastGYM v1.0.0-Beta.02
Setting up a server is one screen: a username, a password, and that
account is the administrator. No token to copy out of a console.The rest is what the first release found the moment it was installed on a
machine that was not mine — an updater that could not write its own new
binary, an installer that asked questions nobody could see, and an install
command in the README pointing at a URL that had never been fetched.Artefacts are signed. The updater checks the signature before it replaces
anything, and you can check it yourself with sha256sum against SHA256SUMS.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
v1.0.0-Beta.01
Pre-releasereleased this
2026-09-01 04:20:17 +02:00 | 97 commits to main since this releaseLastGYM v1.0.0-Beta.01
The first release. Every feature the 1.0 will have is built; what happens
now is real use, the bugs that only real use finds, and the plural forms
for fourteen more languages.Measured on five years of seeded training: 14 MB of binary, 16 MB resident
at rest, 28 ms for the progress screen, 1.6 s for a first synchronisation.Artefacts are signed. The updater checks the signature before it replaces
anything, and you can check it yourself with sha256sum against SHA256SUMS.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)