0.6.0 fixes, bunch 4: front-end bugs and page weight #1226

Merged
tiagoagueda merged 10 commits from b4-frontend into main 2026-11-10 17:55:31 +00:00
Owner

Bunch 4 of the 0.6.0 fixes: front-end bugs and page weight. One signed commit per issue, then one with the changelog lines, the extracted string and what the whole suite found; merge fast-forward-only.

  • Closes #1156: a form htmx sends stays guarded until its answer says it was saved.
  • Closes #1157: save as you go sends a field again when the connection returns, and names a server error.
  • Closes #1154: the errand page polls less often as the errand ages, and ends it at its deadline itself.
  • Closes #1155: one open tab asks for waiting browser notices, through a Web Lock that is given back when the page is left.
  • Closes #1150: the password meter's dictionaries are fetched when the field has focus.
  • Closes #1153: the world's outline is a static file, not inlined in every map page.
  • Closes #1158: stamped avatars and logos are cached privately for a year, and tiles load lazily (not the masthead's).
  • Closes #1149: the page script is served built, without its comments; the source moves to assets/js/app.js and npm run build:js rebuilds app.min.js, which Checks compares.

Tested locally: lint, migration and catalogue checks, the full unit suite, and the full browser suite (956 passed) before the last change to #1155, whose own browser tests and the Back tests were run again after it.

Not built here: the image, whose stylesheet stage now builds the script too. The dev image after the merge proves it. And the Checks job is the first build of app.min.js on Linux: the same bytes are expected, esbuild being pinned.

One new string for Weblate: "Not saved: Postulo answered with an error. Press Save to try again."

🤖 Generated with Claude Code

Bunch 4 of the 0.6.0 fixes: front-end bugs and page weight. One signed commit per issue, then one with the changelog lines, the extracted string and what the whole suite found; merge fast-forward-only. - Closes #1156: a form htmx sends stays guarded until its answer says it was saved. - Closes #1157: save as you go sends a field again when the connection returns, and names a server error. - Closes #1154: the errand page polls less often as the errand ages, and ends it at its deadline itself. - Closes #1155: one open tab asks for waiting browser notices, through a Web Lock that is given back when the page is left. - Closes #1150: the password meter's dictionaries are fetched when the field has focus. - Closes #1153: the world's outline is a static file, not inlined in every map page. - Closes #1158: stamped avatars and logos are cached privately for a year, and tiles load lazily (not the masthead's). - Closes #1149: the page script is served built, without its comments; the source moves to `assets/js/app.js` and `npm run build:js` rebuilds `app.min.js`, which Checks compares. Tested locally: lint, migration and catalogue checks, the full unit suite, and the full browser suite (956 passed) before the last change to #1155, whose own browser tests and the Back tests were run again after it. Not built here: the image, whose stylesheet stage now builds the script too. The dev image after the merge proves it. And the Checks job is the first build of `app.min.js` on Linux: the same bytes are expected, esbuild being pinned. One new string for Weblate: "Not saved: Postulo answered with an error. Press Save to try again." 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The unsaved-work mark of a form with hx-post was dropped twice without
looking at the outcome: by the submit listener as the request left, and
by htmx:afterRequest, which htmx 2 also raises after a 4xx or 5xx it does
not swap, a dropped connection, an abort and a timeout. The form stayed
on the page with what was typed in it, the alert said the request had
failed, and the next link or a closed tab left without a question.

The submit listener now leaves a form htmx sends alone, and afterRequest
lifts the mark only for a successful answer to the form's own submission.
A refusal the server redraws still arrives as a new, unmarked form, so
nothing is asked twice. The comment that said afterRequest is not raised
when the server was never reached is corrected.

Closes #1156

Co-Authored-By: Claude <noreply@anthropic.com>
On the Settings pages that save as they go, a field whose save could not
be sent waited for the person to change it again or press Save: the
connection coming back did nothing. And the answer was read as JSON
whatever its status, so a 500 or a proxy's 502 was reported as Postulo
being out of reach when it had answered.

A field that failed for want of a connection is now remembered on its
form and queued again on the browser's online event and when the tab is
shown again, never on a timer. An answer that is not the view's JSON gets
its own sentence, translated with the page. The status region is written
only when its words change, and a retry does not say "Saving", so a retry
that fails the same way is not announced a second time.

Closes #1157

Co-Authored-By: Claude <noreply@anthropic.com>
The page that watches a piece of slow work asked for its state every two
seconds for as long as the work was unfinished: 1,800 requests an hour
from a tab left open, for up to a day, and for ever on an instance with a
worker and no scheduler, since only the scheduler called an unfinished
errand interrupted.

The wait is now decided by the errand's age: two seconds for the first
twenty, five up to two minutes, fifteen up to ten minutes, then thirty.
The fragment already carries the interval, so the script is unchanged and
the first seconds, when most work finishes, are as quick as before. The
slower poll for a silent worker (#1122) is kept as it is.

The page and its poll also call the one errand they are watching
interrupted once it is past the deadlines the scheduler uses (an hour
after it began, a day after it was asked for), so the page says what
happened and stops asking on the first answer after the deadline. An
errand within its deadline still costs no write.

Closes #1154

Co-Authored-By: Claude <noreply@anthropic.com>
Every open tab asked for the waiting notices once a minute, hidden tabs
included, so five tabs were five signed-in requests a minute for the same
notices, each a delete of the expired ones and, on SQLite, a write
transaction.

The once-a-minute timer now starts inside a Web Lock: the tab that holds
it asks, the others queue for it, and the next one takes over when that
tab closes or moves to another page. A hidden tab can hold it, since a
notification is for somebody who is not looking. Every tab still asks
when it loads and when it is brought to the front, and a browser without
the lock asks from every tab as before.

Closes #1155

Co-Authored-By: Claude <noreply@anthropic.com>
Sign-up, password change, set and reset carried zxcvbn as three deferred
scripts: 1.7 MB (869 kB gzipped) downloaded and compiled by everybody who
opened the page, although the page script only uses them once a password
is being typed.

The meter's template now names the three files in `data-scripts`, still
from Postulo's own origin, and the page script appends them in order the
first time the field has focus (or receives input, for a password manager
that fills it without focusing). When the last one has run, whatever is
in the field is estimated, so a password typed while they were arriving
gets its word without another keystroke; until then the meter says
nothing rather than something untrue.

The display, with its empty status region, is inserted on focus instead
of on the first keystroke, so the region exists before its first word is
written into it and is announced more reliably.

Closes #1150

Co-Authored-By: Claude <noreply@anthropic.com>
With the map's tiles set to None, the companies' map and the page that
places one address each carried the whole outline in a data attribute:
44.7 kB of rings (14.2 kB compressed) on every view, since signed-in
pages are never stored. The rings were parsed from an SVG path in a
template that nothing included any more.

The outline is now static/maps/world-outline.json, the rings exactly as
they were handed to Leaflet, and the one copy of the drawing. The pages
name it in data-map-outline-src and the script fetches it from the
instance's own origin, which the policy's connect-src allows, so a
browser keeps it for as long as the release lasts. It arrives after the
map is drawn, so it is put under what is already there: the pin of the
place page stays on top. jobs/outline.py and the SVG are gone, and the
Natural Earth note moved beside the new file.

Closes #1153

Co-Authored-By: Claude <noreply@anthropic.com>
A person's picture and a company's logo are addressed with a stamp that
changes whenever the picture does, yet both views answered every request
with a day in the cache, so a returning browser fetched each one again
every day. And the tile drew its image with nothing to say it could wait,
so a board or a table of companies fetched every logo on it with the page,
the ones far below the screen included.

A request whose `v` is the picture's current stamp is now answered
`private, max-age=31536000, immutable`; one with no stamp, or an old one,
keeps the day it had, because that address goes on answering with whatever
the picture is now. It stays private in every case.

The stamps are now to the microsecond rather than the second, so that two
pictures set within one second cannot share an address that is kept for a
year.

The tile's image carries `loading="lazy"` and `decoding="async"`. The one
in the masthead's account button is on screen on every page and is left to
load with the page. No width or height: the box is sized by its classes and
the picture is laid over it.

Closes #1158

Co-Authored-By: Claude <noreply@anthropic.com>
app.js is documented where it is written, and every browser downloaded the
documentation: 369 kB, of which more than a third was comments, 101 kB
compressed on a first visit, after every release and in every private window.

The source moves, unchanged, to assets/js/app.js, beside the stylesheet's.
`npm run build:js` writes src/postulo/static/js/app.min.js from it with
esbuild (pinned to an exact version): comments and whitespace gone, syntax
shortened, no name changed, so an error reported from a browser still names
the function it happened in. The built copy is 167 kB, 43 kB compressed, and
it is what base.html loads. With the source out of static/, collectstatic
publishes one copy and not two.

The built copy is committed, so the application still runs without Node, and
is held to its source the way the stylesheet is: tests/test_built_script.py
rebuilds it and compares the bytes wherever `npm ci` has run, CI's Checks job
rebuilds it and fails on a difference, and the image's Node stage builds it
again before collectstatic. Tailwind reads the source for class names and is
told to leave the built copy alone, so the stylesheet does not depend on which
build ran first; the compiled stylesheet is byte for byte what it was.

The unit tests that read the script go on reading the commented source, at its
new path. No source map: it would publish the comments a second time.

Closes #1149

Co-Authored-By: Claude <noreply@anthropic.com>
Record bunch 4, extract its string and settle what the whole suite found
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 15m25s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m7s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m2s
CI / Security audit (pull_request) Successful in 18s
CI / Browser tests (Chromium) (pull_request) Failing after 33m42s
b23f788e82
The changelog lines of the eight front-end fixes before this commit, one per
issue, and the one string they added (the save-as-you-go sentence for a server
error), untranslated: it is translated in Weblate.

What the full unit suite found once the eight were together:
- the GitHub copy of the README, rebuilt after #1149's edit to README.md;
- a test of the people table that pinned the picture tag without the
  attributes #1158 adds;
- the errand page's own deadline check (#1154), recorded with its reason in
  the list of queries made without an owner: the row was fetched through
  for_user by its one caller.

And one change to #1155: the notice lock is given back, and a place in its
queue given up, when the page is left, and asked for again if the page comes
back. Chromium does not keep a page that holds or waits for a Web Lock for
Back and Forward. The built script is rebuilt with it.

Refs #1156, #1157, #1154, #1155, #1150, #1153, #1158, #1149

Co-Authored-By: Claude <noreply@anthropic.com>
Wait for the answer, not the request, in the notice collection test
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 15m23s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m4s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m5s
CI / Security audit (pull_request) Successful in 20s
CI / Browser tests (Chromium) (pull_request) Successful in 33m1s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 16m19s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m3s
CI / Database tests on PostgreSQL (push) Successful in 4m6s
CI / Security audit (push) Successful in 20s
README / README language tables (push) Successful in 17s
CI / Browser tests (Chromium) (push) Successful in 34m13s
Dev image / Dev image: build, scan, push (push) Successful in 46m39s
808fa84216
The script lets one collection finish before it sends the next, so an ask
made while an earlier one is still open is dropped, on purpose. The test
waited only for each request to start. On CI's loaded host the earlier
request was still open when the test rang the timer, the page rightly sent
nothing, and the test waited thirty seconds for it (run of pull request
#1226; it passed again alone).

The test now waits for each answer and asks again if an ask was dropped.

Refs #1155

Co-Authored-By: Claude <noreply@anthropic.com>
tiagoagueda deleted branch b4-frontend 2026-11-10 17:55:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!1226
No description provided.