• v1.0.0-Beta.16 3a6547ad01

    v1.0.0-Beta.16
    All checks were successful
    CI / check (push) Successful in 2m1s
    Release / release (push) Successful in 2m7s
    Docker Release / build-and-push (push) Successful in 4m11s
    CI / tests (push) Successful in 4m21s
    Pre-release

    k4wit0 released this 2026-09-02 17:51:38 +02:00 | 1 commits to main since this release

    Somewhere to set the space, and everyone moved to it

    Added

    • An administrator can change an account's space from its page. The endpoint
      for it shipped in Beta.14 without its control, so the limit could be set over
      the API and nowhere else — which is the same fault as code with no caller.

    Changed

    • The default space is a gigabyte, and accounts that already exist move with
      it. DefaultQuotaBytes is only consulted when an account has no quota row,
      and every account gets a row the first time anything is stored — so raising
      the number in Beta.14 applied to nobody who already existed, and every
      account went on reporting the 512 MB that was in force when its row appeared.
      Migration 019 moves rows still sitting on the old default; only those, and it
      is safe to assume they were never chosen because until now there was no
      control anywhere for choosing one.

    Fixed

    • The administration screen showed a ceiling of zero for an account that had
      stored nothing. It read the quota row directly, and an account gets that row
      the first time something is stored — so a fresh account was shown as
      unlimited while the person themselves was correctly told a gigabyte. Two
      screens of the same server disagreeing, and an administrator would have acted
      on the wrong one.
    Downloads
  • v1.0.0-Beta.15 d66adabfe3

    v1.0.0-Beta.15
    All checks were successful
    CI / check (push) Successful in 1m43s
    Release / release (push) Successful in 2m25s
    CI / tests (push) Successful in 4m19s
    Docker Release / build-and-push (push) Successful in 4m21s
    Pre-release

    k4wit0 released this 2026-09-02 17:32:53 +02:00 | 4 commits to main since this release

    The words your browser was never asked for again

    Changed

    • The plates are a grid of weights rather than a row of pills. A chip is for
      choosing one of a few things with names; this is a dozen numbers, most of them
      on, and equal cells put them under one another where a rack can be checked
      against them.

    • The server's version tile says the version once. The line under it repeated
      the whole string, so on a tagged build — which is every release — it said
      v1.0.0-Beta.14 twice, once large and once small.

    • The disk tile breaks its total down on two lines with an icon each, instead of
      a sentence of small text under the figure. And photographs have their own icon
      now: they were being drawn with the body figure, which is a person standing
      next to a number of kilobytes.

    Fixed

    • Strings added in any release appeared in English for anybody who had used the
      application before it. The service worker caches assets first-hit and keys
      them by URL, which is safe only because every asset carries the build stamp —
      and four fetches did not: the interface locale, both catalogue locales and the
      body figures. So a browser that had loaded es.json once kept that copy
      through every update, and t falls back to the bundled English master for a
      key the cached file has not got.

      Nothing could see it. The audit reads the files on disk and they were
      complete; the scan reads the source and every key existed; a fresh browser
      profile has nothing cached and shows the right words. A test now refuses a
      fetch of a static file without ?v=.

    • A plate typed by hand vanished when it was tapped. The grid shows the
      catalogue plus whatever is owned, so a plate that was in neither list any more
      had nowhere to be drawn, and the only way back was to type it again — a
      catalogue plate taps off and on, a typed one had one life.

    Downloads
  • v1.0.0-Beta.14 2aae4e187c

    v1.0.0-Beta.14
    All checks were successful
    CI / check (push) Successful in 1m37s
    Release / release (push) Successful in 2m21s
    Docker Release / build-and-push (push) Successful in 4m23s
    CI / tests (push) Successful in 4m43s
    Pre-release

    k4wit0 released this 2026-09-02 16:59:24 +02:00 | 8 commits to main since this release

    Your account, your space, and screens that say what they are

    Added

    • A finished session opens with what it was: how long, how many sets, how much
      was moved and how hard it felt. Every one of those numbers was already
      recorded and none of them was shown — the screen was a drawing of food and
      then a list. Under each exercise it now also says what the same exercise
      looked like the time before, which is the difference between a record of what
      you did and something worth reading.

    • Weekly volume is no longer hidden from the interface a new account starts in.
      The argument for hiding it was that a tonnage means nothing without a scale to
      put it against, which is true of a figure and not of a line: whether it is
      going up is exactly the question somebody new has. By movement pattern and Not
      moving stay behind the switch, because those are coaching vocabulary.

    • An administrator can set how much room each account has, and each account can
      see what it is using: a bar with a part for photographs and a part for the
      activity files a watch writes. Two parts because two things charge that
      counter, and an architecture test now refuses a third without one being added
      to the bar in the same change.

      Every account starts with two gigabytes rather than no ceiling, because a bar
      with nothing to fill teaches nobody anything — and the reason to show it at
      all is that finding the limit at the moment of hitting it is finding it too
      late. Zero still means no ceiling, for an administrator who wants one person
      unbounded.

    • A plate the catalogue does not stock can be added by hand. Ticking from the
      list covered almost every gym and bars could already be typed in, so plates
      not being addable was an asymmetry with nothing behind it — and the
      calculator and the warm-up ramp are only worth anything if they work from the
      plates actually on the rack.

    • A destination for notices can be corrected. Adding and deleting were the only
      two operations, so fixing one character in an address meant rebuilding the
      destination — which throws away the record of when it last worked, and that
      record is how anybody diagnoses one that has quietly stopped. An empty secret
      means "keep the one you have", since it is never sent back to the browser.

    • A screen called You, holding the four things that are about the person rather
      than the application: what you are called, how you prove it is you, who else
      may look, and how you take it all with you. They were scattered — a row that
      led nowhere, a section called Security three rows down, a whole navigation
      entry holding one control, and a backup filed next to the bin.

    • The local PIN can be switched on. setPin and clearPin have been complete
      since the phase that wrote them — hashing, an attempt limit, an idle window —
      and nothing had ever called either, so the lock screen worked perfectly and
      could not be reached. Verified by driving it: set a PIN, push the idle stamp
      back an hour, come back, and the application is behind the curtain.

    • PATCH /api/me: somebody may now change their own display name and their own
      password. Neither was possible without an administrator, which quietly worked
      against the promise this project makes in writing — that an administrator
      manages accounts and the server and never reads anybody's training. A design
      where the only route to your own password runs through another person makes
      that promise harder to keep than it needs to be.

      The two halves are not held to the same standard. A display name is cosmetic
      and reversible, so a live session is enough. A password is the credential, and
      changing one is exactly what somebody does with a stolen session, so it asks
      for the current password first — the way turning off the second factor already
      does. The username is deliberately not changeable: it is the handle the audit
      log records, and a record of who did what is worth nothing if the name in it
      can be edited afterwards.

    Changed

    • Comparing two photographs is done by looking at them. The two were picked
      from a segmented control with one pill per photograph, each carrying a date:
      fine with four and a wall of identical grey dates with forty, and the wrong
      question anyway — nobody remembers a physique by its date. It is two rows of
      thumbnails that scroll sideways now, so forty take the same vertical room as
      two.

    • The body screen no longer says "Derived" over two numbers. The word reads as
      something out of a maths lesson, and what the row is for is the figures
      themselves; the sheet behind them still explains where each came from.

    • Disk usage has left the body screen for the account screen. It is a fact
      about an account rather than about a body — it counts photographs and the
      files a watch writes — and it now sits beside the limit that gives it
      meaning.

    • Train answers the same question Today does. Today said "Push · 5 exercises"
      with a button on it while Train, one tab across, said nothing was on the go
      and offered a blank session — so tapping the training tab looked like being
      told there was nothing planned. The plan leads there now, and recording
      without one stays underneath, because somebody who does not follow a routine
      is a whole user rather than a degraded case.

    • Routines names both of its halves. It holds two different things — the
      routines that belong to you, and the programme shapes anybody can take a copy
      of — and only the shapes had a heading, so the routines read as the screen
      itself and the shapes read as five more of them. Reported as "one running and
      five more below", which is exactly what an unlabelled list above a labelled
      one says.

    • Notices no longer keep a log on the settings screen. Nobody can act on
      "eleven were delivered"; they can act on some having been given up on, and on
      why. That is one line when it happens and nothing when it does not.

    • The green "notifications are on" banner is a row. A banner at the top of a
      screen reads as the most important thing on it, and this was the least: it
      told somebody what they had just done.

    • Destinations and push devices are rows that lead somewhere, with the
      destructive action inside rather than a red button pressed against the name.

    • "Devices" and "On this device" were two screens saying much the same thing,
      and are now one. What the first had that was worth keeping is a single line —
      whether anything is still waiting to reach the server — and it showed the
      sequence number and the sync epoch instead, which are for debugging the
      protocol.

    • The server screen's facts are tiles with their own icons rather than a list,
      and the schema number is gone from it: it matters while a migration runs and
      is otherwise a number with no action attached. The audit log is called
      Records.

    • Privacy is no longer a place in the navigation. It held one control, which
      most people use once and many never use at all, and it now sits on the account
      screen beside the question it belongs with.

    Fixed

    • i18n:scan could not see any key that takes a placeholder. Its pattern
      required a closing bracket straight after the string, so t("key") was
      checked and t("key", { count }) was not — seventy call sites, and the ones
      most worth checking: a missing label is visible, a missing sentence with a
      {count} in it needs somebody standing on the right screen.

    • lastgym doctor was quietly deleting activity files from everybody's usage.
      The recompute summed progress photographs alone and had done since before
      cardio existed, while cardio uploads charge the same counter — so rebuilding
      somebody's usage discarded every file their watch had ever contributed. It
      wrote the answer back, and it did that without --fix, so a command whose
      whole job is to look was corrupting the number it was looking at.

      Activity files now carry their size the way photographs always have
      (migration 018), the recompute sums both, and doctor fills in the sizes of
      files stored before there was a column to hold them.

    • Your date of birth came back empty every time. It saved correctly and could
      not be drawn: born_on is a moment, so it arrives as
      "1990-05-12T00:00:00Z", and an <input type="date"> takes exactly
      yyyy-mm-dd and ignores anything else without saying so. Anybody who set it
      reopened the screen, saw a blank field, and had every reason to think it had
      not been saved.

    • On a desktop the navigation showed itself twice. Past 60rem the bar becomes a
      sidebar with every destination on it, which is the point of having the room —
      but the More button stayed, sitting under the full list and offering a sheet
      containing the same list again.

    • A role noun stood in for a person the server has never met. The line that was
      drawn in Beta.13 — a role noun stays because it is a language's unmarked word
      for the job — turns out to have a second half. "Give a coach access" names the
      job. "Has my coach been looking?" names somebody, and eight languages put a
      gender on them: mi entrenador, mein Trainer, ο προπονητής μου, мой тренер.

      Where the language has a natural way to say it, the person is now named by
      what they do: quien me entrena, chi mi allena, quem me treina, όποιος με
      προπονεί, la personne qui m'entraîne. German, Polish and Russian have no short
      neutral for it, so those ask what the audit log actually answers — whether
      anybody read it — and their notification title names the session instead.

      The role labels themselves are unchanged, and so are the generic mentions.

    Downloads
  • v1.0.0-Beta.13 997cfda993

    v1.0.0-Beta.13
    All checks were successful
    CI / check (push) Successful in 1m48s
    Release / release (push) Successful in 2m14s
    Docker Release / build-and-push (push) Successful in 4m11s
    CI / tests (push) Successful in 4m36s
    Pre-release

    k4wit0 released this 2026-09-02 12:18:15 +02:00 | 23 commits to main since this release

    Twenty languages that do not guess who you are

    Added

    • The plural forms for the fourteen languages that were still missing them —
      616 strings, written by hand. Every language is now at 100%: Arabic with all
      five of its categories including the dual, Polish and Russian with three
      each, and eleven more with their singular.

      This is the thing the base keys could not do. A count of one rendered the
      plural — "1 series", "1 sesji", "1 подходов" — on the screen of anybody who
      trained once in a month, and no test catches a sentence that is merely wrong
      in a language nobody on the team reads.

    • bun i18n:stale, the check the other three could not make: a translation of
      an English string that has since been rewritten. It is a complete, correct
      sentence in the right language, so nothing missing, nothing left in English
      and nothing empty all pass — and what is on screen answers a question that was
      asked a version ago. It reads git: if English moved after a language did, that
      language is behind. An acknowledgement in scripts/i18n-stale-ok.json names
      the English commit it was made against, so a cosmetic rewrite can be signed
      off without silencing the next real one.

    Changed

    • Five strings are back to their natural wording, and on record in
      scripts/i18n-identical.json as legitimately identical to English: the Dutch
      and Turkish singular of a set, and the Italian singular of a file. All three
      languages take the English loanword there, so the honest translation is the
      English word — and padding it out to get past the audit is answering the tool
      rather than the reader. That file exists for exactly this.

    • Every empty screen now speaks in the other eighteen languages. The bin, the
      photographs, the month with nothing in it, the session that recorded no sets,
      the progress screen before the first workout — all of them had been rewritten
      in English and Spanish and left as captions everywhere else, so a character
      drawn on the screen stood over a sentence describing a table. 321 strings
      across eighteen files.

    Fixed

    • Ten languages assumed the reader is a man. English says "you trained" and asks
      nothing about who you are; Arabic, Bengali, French, Greek, Hindi, Italian,
      Polish, Portuguese, Russian and Urdu cannot say it without agreement, and each
      had picked masculine and left it there. Every one is rewritten so the sentence
      no longer needs the agreement — never with a slash, which is a typographic
      apology rather than a fix.

    • The first screen of a new server greeted her as a man. Bem-vindo and
      Benvenuto on the account-creation screen, before there is an account with a
      gender to assume. Spanish had solved this long ago with "Te damos la
      bienvenida"; the other two now do the same, with the agreement on the noun.

    • lastgym bench measured every query on the progress screen except the one
      with a promise attached. The load path — the band the screen draws above
      everything else, and the only thing on it whose closing criterion names a
      number — was not in the list, so the command reported a comfortable total for
      a screen it had not finished timing.

    • The photographs screen blamed a photograph when the connection was what had
      failed. With no network the list never arrives, and the screen said "that
      photograph could not be loaded" — about a picture nobody had asked for yet. It
      now says what is actually true, which is a decision and not an accident:
      photographs live on the server and are never copied to the device, because
      they are fetched with credentials and drawn from a blob URL revoked as soon as
      they are on screen, so nothing of anybody's body lands in a cache on a phone.
      Seeing them needs a connection, and the screen says so.

    • The audit log read backwards in Arabic. Not the words — the direction. The
      screen substituted the two account names into the translation itself instead
      of letting t do it, which skipped the bidi isolates t puts around every
      value, and a Latin username inside an Arabic sentence then dragged the
      punctuation to the wrong side. Rows starting with a name anchored left while
      rows starting with an Arabic word anchored right, in the same list. Six other
      screens were substituting a date, a number or lastgym user the same way; all
      of them go through t now, and a test refuses the second interpolator rather
      than any particular string.

    • Persian and Urdu addressed one reader in two registers. Five strings in
      Persian and nine in Urdu used the formal شما and آپ while the fifty-odd around
      them used تو and تم — the same handful of keys in both files, and the same
      ones Bengali had, so one translation pass carried the register in. On the
      privacy screen the heading and its own first line disagreed.

    • The audit log named a gender for everyone in it. "{actor} opened the account
      {subject}" has no gender in English, and none in Spanish, Portuguese, German,
      Dutch, Hindi or Urdu either — their past tenses do not mark it. Arabic, Polish
      and Russian conjugate it, and Greek was writing "Ο/Η {actor}", the slash this
      work exists to avoid. Those four logs now read as events rather than sentences
      about people: "{actor}: opening of account {subject}". The register was already
      in each file — a refused sign-in has no actor to conjugate, so that one row had
      been written in the passive from the start.

    • Thirty-nine other strings assumed the gender of somebody who is not the
      reader: the coach, the administrator, the account being deleted. German said
      "Er hat keine Möglichkeit" of an administrator it has never met, French asked
      "mon coach a-t-il regardé ?", Urdu had the coach putting a routine in your
      account as a man. English uses singular "they" throughout and the translations
      had not followed.

      Role nouns are deliberately left as they are — Trainer, προπονητής, منتظم. A
      role noun is a language's unmarked word for the job, and the only ways to
      neutralise every one of them are doublets, which is the slash, or coinages
      nobody uses. That is a different question and it is not this one.

    • Arabic put plurals after a hundred. In Arabic 11 and above take the singular,
      so "{days} أيام" was wrong at every number the plural form exists to cover —
      twenty-two strings said "100 days" the way no Arabic speaker writes it.

    Downloads
  • v1.0.0-Beta.12 2a5bab0e9a

    v1.0.0-Beta.12
    All checks were successful
    CI / check (push) Successful in 2m2s
    Release / release (push) Successful in 1m52s
    Docker Release / build-and-push (push) Successful in 4m0s
    CI / tests (push) Successful in 4m30s
    Pre-release

    k4wit0 released this 2026-09-01 21:15:19 +02:00 | 35 commits to main since this release

    The diagnostic comes out, and the rule it disproved with it

    Removed

    • /insets.html and its manifest. They answered the question they were added
      for: on an iPhone the tab bar is 54 points with its label four off the bottom
      edge, both safe-area insets report zero, and the viewport is 47 points shorter
      than the screen — the status bar, which iOS keeps rather than hands over. The
      bar that was reported as too tall and detached was an older build; it had been
      fixed for two releases and could not be seen, because until Beta.07 a new
      build only reached the installed application on the second launch.
    • The rule that made the shell full height when installed. It was written on a
      theory — that 100dvb reports the safe height and leaves the bar floating —
      that the phone then disproved: 100dvb, 100% and 100vh all report the
      same number there. A rule that does nothing, with a comment explaining why it
      was needed, is worse than no rule.
    Downloads
  • v1.0.0-Beta.11 dded9e2049

    v1.0.0-Beta.11
    All checks were successful
    CI / check (push) Successful in 1m49s
    Release / release (push) Successful in 2m3s
    Docker Release / build-and-push (push) Successful in 3m57s
    CI / tests (push) Successful in 4m30s
    Pre-release

    k4wit0 released this 2026-09-01 20:41:45 +02:00 | 38 commits to main since this release

    The diagnostic measures the real navigation

    Fixed

    • /insets.html renders the application's own navigation, in its own markup
      and drawn by its own stylesheet, instead of a hand-made lookalike. The
      lookalike had a label where the real one has an icon above a label, so the
      two were never the same height and comparing them by eye proved nothing. The
      height it reports is now the application's height, on the phone that is
      reporting it — along with how far the label sits from the bottom edge, and
      what the bar's bottom padding actually resolved to.
    Downloads
  • v1.0.0-Beta.10 c04499483a

    v1.0.0-Beta.10
    All checks were successful
    CI / check (push) Successful in 1m27s
    Release / release (push) Successful in 2m28s
    Docker Release / build-and-push (push) Successful in 4m9s
    CI / tests (push) Successful in 4m48s
    Pre-release

    k4wit0 released this 2026-09-01 20:20:52 +02:00 | 41 commits to main since this release

    The diagnostic opens the way the app does

    Fixed

    • /insets.html carries a manifest of its own, so adding it to the home screen
      actually opens it standalone. Without one iOS ignores the old Apple meta tag
      and opens it in Safari — toolbars, no safe-area insets, and a viewport 47
      points short of the screen, which is exactly what the first run reported. It
      also says out loud which mode it is in, so a screenshot of it cannot be
      ambiguous.
    Downloads
  • v1.0.0-Beta.09 b53f74c18a

    v1.0.0-Beta.09
    All checks were successful
    CI / check (push) Successful in 1m40s
    Release / release (push) Successful in 1m58s
    Docker Release / build-and-push (push) Successful in 4m16s
    CI / tests (push) Successful in 5m14s
    Pre-release

    k4wit0 released this 2026-09-01 20:05:24 +02:00 | 45 commits to main since this release

    A page that asks the phone what it reports

    Added

    • /insets.html, temporarily. A page that reports what a phone actually says
      about itself — whether it is running standalone, the viewport height by three
      different measures, the safe-area insets, and the geometry of a tab bar built
      the same way the application's is. Added because the tab bar looks wrong on
      an installed iPhone and nothing available here reproduces it: Chrome does not
      emulate display-mode: standalone, and WebKit on Linux has no safe areas at
      all. It goes away once it has answered the question.
    Downloads
  • v1.0.0-Beta.08 c2dd2a227b

    v1.0.0-Beta.08
    Some checks failed
    CI / check (push) Successful in 1m15s
    Release / release (push) Successful in 1m44s
    Docker Release / build-and-push (push) Successful in 3m17s
    CI / tests (push) Failing after 4m56s
    Pre-release

    k4wit0 released this 2026-09-01 19:40:35 +02:00 | 48 commits to main since this release

    The tab bar stops spending the safe area twice

    Fixed

    • The tab bar no longer spends the safe area twice on an installed iPhone.
      100dvb is the right height in a browser tab, where the address bar comes
      and goes and a fixed bar must follow it; installed there is no address bar
      and it reports the safe height instead, stopping short of the home
      indicator. The bar was fixed to the bottom of that — already 34 points above
      the real edge — and then added its own 34 points of inset, because nothing
      told it the first 34 were gone. Measured from the label to the bottom edge:
      about 45 points on Android against about 98 on an iPhone, where the two home
      indicators account for roughly ten. Full height only when installed, so the
      browser-tab behaviour is untouched.
    • The tab bar is the height a tab bar is. Its band of icons and labels was 62
      points where iOS gives one 49; eight points of padding became four and it is
      54 now. The 48-point target is untouched — min-block-size is on the border
      box — and every visible tab measures 53 after the change.
    Downloads
  • v1.0.0-Beta.07 ae6ce1c36a

    v1.0.0-Beta.07
    Some checks failed
    CI / check (push) Successful in 1m37s
    CI / tests (push) Has been cancelled
    Release / release (push) Successful in 1m50s
    Docker Release / build-and-push (push) Successful in 3m44s
    Pre-release

    k4wit0 released this 2026-09-01 19:14:59 +02:00 | 53 commits to main since this release

    A new build lands on the first launch, and the door is centred

    Fixed

    • The installed application picks up a new build on the first launch after it,
      instead of the second. Assets are stamped with the build precisely so a new
      one cannot be served from an old cache, and the service worker matched them
      with ignoreSearch — which throws that stamp away, so /app.css?v=new
      matched the cached /app.css?v=old. A navigation is network-first, so the
      fresh HTML arrived and asked for the new stylesheet, and the previous build's
      one came back. Every fix landed one launch late, which from the outside is
      "I updated and it still looks the same".
    • The sign-in card sits in the middle of the screen. It went straight into the
      scroll container and therefore straight to the top of it, with the mascot
      tucked under the status bar and two thirds of the display empty below the
      button. Centred with safe alignment, so a short screen or a keyboard over
      half of it pushes the card down rather than putting the username field out of
      reach above the top.
    Downloads