mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
main
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13303b247e |
v0.3.9 milestone (#283)
* Run the end-to-end suite on pull requests into milestone and wip branches
A release's work is now collected on a milestone branch (milestone/v0.4.0)
before it reaches main, and the suite only ran on pull requests into main,
so nothing proposed for the milestone was tested. `milestone/**` joins
`main`, and `wip/**` comes along from wip/28, where it already runs the
suite on the phases of #28 — the same block on both, so they merge
without a conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
122c8e56f3 |
Publish a GitHub Release for every v* tag, with its CHANGELOG section as notes (#240)
A tag gave a feed reader nothing but a version number: there was no releases.atom and nothing for "Watch → Releases" to notify about. A new `release` job in docker.yml runs after the image is published, cuts the tag's section out of CHANGELOG.md with bin/release-notes.sh (unwrapped, since a release body shows every newline), and creates the release, or rewrites its notes on a re-run. Discussion #226, asked by @adrianfurgol. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e23377eb6a |
Migrate test databases with app:db:migrate so the cache tables exist (#225)
doctrine:migrations:migrate leaves cache_items out; without it the first cache read inside a test's transaction aborts it, so ThreadSummaryTest and the Gmail import tests failed whenever nothing had created the table first. |
||
|
|
8c2ef7663b |
feat(ops): a demo deploy that cannot quietly do nothing
Three commands in the handbook, one of which fails silently.
`docker compose pull` can run out of disk part-way through extracting a
layer. `up -d --wait` then brings the stack up on the image that is
ALREADY there and reports every container Healthy — so a deploy that
changed nothing is indistinguishable from one that worked, and the only
evidence is a line buried in the pull output. v0.1.36 was believed
deployed for twenty minutes while the demo went on serving v0.1.35.
Piping made it worse. Through a pipe the exit status belongs to whatever
came after it, so the `pull | tail -2` in the handbook's sequence
reported success no matter what the pull did. Nothing in the script is
piped.
And the disk fills on its own: every deploy leaves its predecessor behind
and nothing collects them. Eleven orphaned images is what filled it, on a
host that also runs Apache, MySQL and ClamAV and never has more than a
few GB spare.
So: check for room, prune if short, pull and STOP if that fails, up,
then clear the images the deploy replaced.
· `-a` on the prune is load-bearing. A replaced release is a fully
tagged image nothing references any more, and dangling-only leaves
every one of them.
· `--volumes` is never passed. The demo's database is in one.
· compose.proxy.yaml is included by existence rather than by name: it
belongs to a host that already has something on port 80 and is not in
this repository.
Verified on the real demo host, both ways — a normal run reports "image
unchanged" and exits 0, and DEMO_MIN_FREE_MB=999999 refuses, exits 1, and
leaves the running stack alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
24a5ab3de1 |
build: mercure 0.8, and a database of PHPUnit's own
MERCURE 0.5.0 / symfony/mercure 0.8.0, kept out of the earlier sweep because it carries the live-update transport and should not ride along with eighteen patch bumps. It earned that: HubInterface gained getProtocolVersion() and getCookieName(), so the two hub doubles in CalendarSyncFinishedNotificationTest stopped satisfying it — a fatal error, not a failing test. Both implement them now, returning what a real hub would rather than pretending to configuration these tests do not have. Verified where it matters: the full browser suite, including the specs that stop and restart the hub to prove the stream recovers. A DATABASE OF ITS OWN for the unit suite. Both suites run APP_ENV=test, and doctrine.yaml's `dbname_suffix: '_test%env(default::TEST_TOKEN)%'` therefore pointed both at app_test. One database, two suites — and one test in here truncates real tables, because exercising a data reset is its whole purpose. Sequentially that is safe; Postgres rolls a TRUNCATE back like anything else. Concurrently it is not: TRUNCATE takes an ACCESS EXCLUSIVE lock, so live queries from the browser suite block until the unit test's transaction ends, and specs with no connection to any of this report timeouts. It fits the transaction abort seen here once and never reproduced. TEST_TOKEN is Symfony's own mechanism for exactly this, so the fix uses it rather than inventing a second DATABASE_URL. bin/test-unit.sh sets it and does the create/migrate/seed a fresh database needs. The seeding is the part worth guarding: PageRendersTest, ListFragmentTest and others SKIP THEMSELVES when the admin user is missing, so without it a green run would quietly be ~50 tests smaller than it looks — a worse failure than a red one. Checked rather than assumed: 4047 tests either way, no skipped or incomplete markers. The assertion count moved, 11540 → 11526, and settled there across runs. That is the point of the change showing up: the unit suite was reading rows the browser stack had left behind, and now it reads only its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f6aeb02e0d |
Give the landing page a way off itself on a phone
You were right, and I had it backwards. I left the burger off the landing page on the grounds that it has no sidebar to open — but .topbar-links are hidden at that width too, so the front page offered no navigation at all. Somebody arriving from a link had a page and no way anywhere else. It needs the drawer MORE than a handbook page does, not less. The drawer is on every page now, carrying the handbook navigation plus the bar's own links, and it is hidden above the breakpoint where the sidebar would have no business sitting beside a hero. That hiding is scoped to a min-width query rather than left to source order. Written as a plain rule it sat after the narrow block, matched at every width and won on order — so the first version of this put a burger in the phone's markup that nothing could click, which is exactly the bug being fixed, one layer down. On the horizontal scrolling: the document itself does not pan, at 320, 390 or 393. What moves is a <pre>, which scrolls inside its own box by design — a wrapped shell command is a command somebody pastes wrong. But `overflow-x` was `visible`, so anything that overflowed in future WOULD take the page with it. It is `clip` now, not `hidden`: hidden would make the element a scroll container and silently break the sticky top bar, which is the thing the fix was protecting. And the image build skips documentation. A handbook page, a translation or a change to the site tooling cannot alter the image, and rebuilding it for two architectures to publish identical bytes is the most expensive way to do nothing. CHANGELOG.md is deliberately not in the ignore list — a release commit always touches it, so the release path can never be filtered out by a rule aimed at prose — and workflow_dispatch is there for the case that reasoning misses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5bda9e9db8 |
Stop the landing page being 1331 pixels wide on a phone
Two causes, and the first is the kind of bug that hides behind a reasonable-looking line. `.layout` carries `align-items: flex-start`, which is right in the row layout — it stops the sidebar stretching to the article's height. On a phone the same container becomes a column, and there `flex-start` no longer means "top": it means "as wide as your content". So <main> sized itself to its widest unbreakable line and came out 1331px wide inside a 393px viewport. The handbook did not show it because .prose caps at 46rem there, so it was merely wrong by less — which is why the phone tests passed while the front page was unusable. The second was a long URL inside inline code. It has nowhere to go and pushed a 320px phone over by seven pixels. Inline code may break now; code inside a <pre> deliberately may not, because that block scrolls and a wrapped shell command is a command somebody pastes wrong. The overflow check now runs over the landing page, the handbook index, a long install page and a German provider page rather than one page that happened to be fine, and reports how many pixels over it went. Both failures were silent, and a page that scrolls sideways looks broken before a single word of it is read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5d83c4d5e9 |
Make the site usable on a phone
Three things, and the middle one is the reason the other two matter. **The sidebar was a wall.** Below the breakpoint it became a full-width block ABOVE the content, so reaching the first paragraph of any page meant scrolling past thirty-four links. It is an off-canvas drawer now, opened by a burger, closed by the burger, the scrim, Escape, or following a link — which navigates, and closing first stops the old page's drawer animating away over the new one. **Both pickers moved into the foot of the drawer.** Four controls do not fit across a phone: with all of them in the bar the search box was squeezed to twenty-four pixels, which is not a search box. They are moved rather than duplicated, because two copies means two elements sharing an id and neither picker working, and moved by script because no stylesheet can reparent a node. The bar now holds a burger, a wordmark and a search box. **Zoom.** The viewport carries user-scalable=no as asked, and it is worth knowing that is half a measure: iOS Safari has ignored it since iOS 10, and where it IS honoured it is a WCAG 1.4.4 failure. What actually stops a phone zooming unbidden is the 16px minimum on inputs — Safari zooms the page to focus anything smaller — so that is the part carrying the weight. Covered by tests/e2e/site.spec.ts, still opt-in and still not in CI. Two things worth recording from writing them: `devices["iPhone SE"]` is the FIRST-generation SE at 320×568, so a tap coordinate written as 350 was off-screen and silently hit nothing; and the scrim covers the viewport, so its centre is under the open drawer and a locator click is refused. The tap is derived from the drawer's own edge now. One self-inflicted wound worth naming: rewriting the narrow-screen block by slicing from the media query to end-of-file also deleted the picker and fallback-notice rules that had been appended after it, which put "Theme" and "Language" on screen as visible text. Found by looking at a screenshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e6d0e9ca5d |
Keep the reader in the language they chose, and publish faster
**The picker was not the bug; the navigation was.** Every sidebar link on a German page was built against the English path, so choosing German lasted exactly one click and then put you back in English. The sidebar and the Handbook link now follow the page's own language. **And the choice survives arriving somewhere else.** It is remembered, so a bookmark, a link from somebody else or a fresh visit lands in the language you picked rather than resetting to English every time. The redirect cannot loop — it lands where the page's language already equals the remembered one — and it cannot strand anybody, because choosing English is what sets the memory to English. The landing page is exempt: following the memory there turned every visit to the site into a redirect into the handbook, which is a redirect away from the page somebody asked for rather than a language preference. tests/e2e/site.spec.ts covers all of it — the two bugs you found by clicking, plus the three ways the fix could go wrong. It is opt-in behind E2E_SITE (`npm run test:e2e:site`) and does not run in CI: it needs a PHP build and eight browser pages, and what it guards is a static site whose failure is visible the moment anybody opens it. **Publishing is faster.** The Pages job ran composer.json's auto-scripts on every docs change — cache:clear, assets:install and importmap:install, which is a Symfony kernel boot and a set of CDN downloads for JavaScript, to render Markdown. --no-scripts, plus a download cache. The wiki job needs no Composer at all and never did: bin/mirror-wiki.php has no dependencies, which is now said out loud so nobody adds one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d3c1266ad |
Send the wiki and the language picker where they meant to go
Two things German broke, both mine. **The wiki build.** bin/mirror-wiki.php resolved a translated page's links against its own directory, where bin/build-site.php and DocumentationCoverageTest both resolve against the English tree. It was the odd one out, and nothing noticed until there was a translation to be odd about: from docs/de/features/, `../screenshots/inbox.png` is docs/de/screenshots/ and does not exist, and from docs/de/install/, `../../CONTRIBUTING.md` lands one directory short of the repository root. Twelve links, and the check refused to publish — which is the check doing its job, so this is a fix to the script and not to the gate. Having resolved through the English tree, a link now lands on the reader's own language wherever that page exists. Otherwise every link in the German handbook would have walked them back into English. **The language picker did nothing on the landing page.** It rewrites the /docs/ segment of the path, and the landing page has none — so the switch matched nothing, the browser went to the identical URL, and it looked like a reload. The landing page is README.md and has no translation, so there is no German counterpart to send anybody to; it now goes to the handbook's index in that language, which is where somebody changing the language on a front page was headed anyway. Both were found by looking rather than by the suite: the wiki failed loudly in CI, and the picker was reported by the person clicking it. Neither is covered by a test, which is worth saying — the render check catches a dangling link but not a link that resolves somewhere useless, and nothing at all exercises the picker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
629d937d97 |
Translate the handbook into German
All 34 pages, under docs/de/. The site serves them at /docs/de/, the language picker switches between them, and a page that ever falls behind can simply be deleted — the English falls back with a banner saying so. The handbook duzt. plMail is something a person runs for themselves on their own machine, and a manual that siezt is describing a different product. That correction arrived after four of the six translations were drafted, and those pages were rewritten rather than patched: a handbook that switches register halfway is worse than either choice consistently. Two defects in the coverage test, both found by translators hitting them: The link check resolved a translated page against the English tree by replacing '/docs/de/' inside the DIRECTORY — which never fires for a page sitting directly in docs/de/, whose directory has no trailing slash. Every page one level down passed and only the language's own index failed, so the bug was invisible in exactly the files anybody writes first. Derived from the relative path now. The traps-section exemption was keyed to literal English paths, so docs/de/CLIENT_DEVELOPMENT.md was required to carry a section its own source is excused from. A translator dutifully invented one, which is the failure mode worth naming: a test that demands nonsense usually gets it. The exemption follows a page into its translations now, and the invented section is gone. And one in the site builder, which was breaking English pages too. Links carrying a fragment were never rewritten from .md to .html — the greedy target group swallowed the '#', realpath could not resolve what was left, and the link was emitted untouched. Every cross-page anchored link in the handbook 404'd while the same link without an anchor worked. The site's chrome is translated too. A German page framed in an English search box says the translation was an afterthought. plMail's own German is now consistent with it: fifteen strings in the calendar and integration screens siezten while the rest of the catalogue duzt. They were written today, which is how they diverged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
af3265a045 |
Make a translation possible, and default the site to Solar
Three things, all in service of the German handbook that follows. **A language is a directory.** docs/<locale>/ with the same paths as the English tree, and the site renders a language from the ENGLISH page list, falling back to the English page for anything not translated and saying so in a banner in the reader's own language. Without that fallback a language is all-or-nothing, which means it is never begun; with it, the first page translated is the first page somebody reads in German and the rest still resolve. It also makes deleting a bad translation a safe fix rather than a hole. **A translation says what it was made from, and a test checks.** Each translated file opens with `<!-- translated-from: <path> sha1:<hash> -->` and DocumentationCoverageTest re-hashes the English. This is the only check here aimed at truth rather than inventory, and it gets at it sideways: nobody can test whether a German paragraph still describes the code, but a translation made from a page that has since changed is a claim about the past, and that is checkable. A stale translation is worse than a missing one — the missing page admits it, the stale one is fluent and wrong. A content hash, not a git blob hash and not a commit date. Dates mark every translation stale after a whitespace sweep, and git is unusable from the test container, which refuses this repository for dubious ownership — a failure this suite has already had once. The other checks are now locale-aware: a translated page is not required to appear in the English index (its position is its path), its traps section is looked for under the fixed heading its language uses, and its links resolve against the ENGLISH tree — which is what the site does too, and what stops a partial translation demanding that every page it links to is already translated. **Solar is the site's default theme**, and "System" now means something. There was a [data-theme="dark"] block and no prefers-color-scheme query at all, so System was light on every machine — a switcher option that silently did nothing. The media query is scoped :not([data-theme]) so an explicit choice still out-ranks the operating system, or Solar would lose to a dark-mode laptop. The README now points at the handbook above the fold, phrased as a link to the handbook rather than to "our website" — the same line is the top of the landing page that README generates, where "visit the site" would be addressed to somebody already on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3f9f5e34b9 |
Make /docs/ a page rather than a 404
The handbook's index rendered as docs/README.html, which is a faithful translation of the file name and the wrong URL: every "Read the handbook" link points at the directory, and so does anybody who trims a page's URL back a level. Both got GitHub's 404. README.md in the docs root now becomes index.html, and the link rewriter applies the same rename to whatever pointed at it — otherwise the pages that link back to the index would all point at the file that no longer exists. Found by asking for the URL rather than trusting the deploy's green tick. |
||
|
|
972a8a9d3f |
Give plMail a landing page, built from the repository it describes
A wiki is where somebody already inside GitHub looks. It has no way to be the thing a link to plMail shows a person who has never seen it, so there is now a site as well: a landing page and the whole handbook, at karatektus.github.io/pl_mail, deployed by .github/workflows/pages.yml on every push to main. It holds no content. The landing page IS README.md and the handbook IS docs/, rendered into a shell by bin/build-site.php — nothing on the site can say something the repository does not, because there is nowhere for it to be written down. Even the hero is lifted rather than written: its lead is README's own opening paragraph, because a hero composed here would be the one piece of prose no file backs and the first thing to go stale. The wiki mirror stays; two outputs of one source cannot disagree with each other. The tree is preserved rather than flattened, unlike the wiki mirror, which had to flatten because a wiki has one namespace. So a cross-link between two pages needs only its extension changed and an image reference needs nothing, and the rewriting that remains is the small honest kind: a path that escapes docs/ becomes a link into GitHub, because the site carries the handbook and not the source. **The theme picker is generated from the app's own enum.** Theme::swatch() already carries surface, ink and accent per theme for the appearance picker, and the rest of a palette is derived from those at build time — so the site offers exactly the seven plMail offers, and adding an eighth to the app adds it here. Transcribing them would have agreed until somebody added one, and then the site would quietly offer six of seven. The language picker lists a locale as available when docs/<locale>/ exists and as "not translated yet" when it does not, which makes translating the handbook the act of creating that directory and nothing else. Today that means English, with German and Pirate shown and disabled — a switcher that silently omitted them would say the site has no opinion about German, where this says where the gap is. Search is the whole handbook as 140KB of JSON, fetched on first focus and matched in the browser. A hosted index would be faster to write and would stop the site being a directory of files that works from a checkout, from a fork's Pages, and behind whatever proxy somebody puts it behind. league/commonmark is a require-dev dependency: the site is built in CI and never by the application, so a Markdown parser in the runtime image would be a package nothing runs. Bumped guzzlehttp/guzzle to 7.15.2 while composer was open — 7.15.1 carries two advisories, one of which is a noncanonical host bypassing host-based checks, which is the shape of the SSRF guard. Also corrects this file's claim that the non-Docker Playwright variants start `symfony serve`. They have not since that server was replaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d103cfbdf |
Write the handbook, and make it fail the build when it goes stale
README says what plMail is and gets you running; CONTRIBUTING says how to
change it. Nothing covered the part in between — using it, operating it,
and registering the Google and Microsoft applications it talks to. That
is docs/: 34 pages across features, installation, providers and
internals, mirrored to the GitHub wiki.
The wiki is a mirror and never a source. bin/mirror-wiki.php flattens the
tree into the wiki's flat namespace, rewrites every relative link,
generates the sidebar from the index so the two cannot disagree, and
stamps each page with a footer saying where it came from. A browser edit
survives until the next push to main. That cost is deliberate:
documentation not versioned with the code drifts from it, and the drift
is invisible until somebody follows an instruction that stopped being
true.
Which is the whole problem, so the checkable half is checked.
DocumentationCoverageTest fails the build when a command is missing from
the reference or documented and gone, when a variable in .env is absent
from the configuration page, when a relative link dangles, when a page is
unreachable from the index or promised and missing, or when a page has no
"Things that bite" section. It cannot check whether a paragraph is TRUE,
and says so — this is the floor, so review attention goes to the part a
machine cannot have an opinion about. CONTRIBUTING has claimed since it
was written that the command table "is part of the definition of done";
nothing enforced it until now, and it was wrong in three places.
Writing it found things. They are fixed here rather than documented
around, because a handbook describing a bug accurately is still a
handbook nobody can follow.
compose.yaml mounted no volume for var/attachments, var/raw
or var/uploads. Workers write those during sync
and the web container serves them back, so each
kept its own copy in its own layer — downloads
404, and everything died on a recreate. The
override and the TrueNAS file both mount them;
the file the README tells an operator to run did
not. The same omission cost real data once
before. See the CHANGELOG for moving what is
currently stranded.
app:calendar:materialise new, nightly. Occurrences are drawn when an
event is saved and nothing moved that window
again, so a weekly standup ran out of dates in
two years and its reminders stopped with them —
silently. The query it runs had existed since
the materialiser did, documented as exactly this
sweep, and had never been called.
Microsoft wizard asked for three of the nine permissions plMail
requests, and a token already issued is not
upgraded when the rest are added later.
GmailWatchService told you to grant Pub/Sub Publisher to
gmail-api@system.gserviceaccount.com. The
account is gmail-api-push@; a topic granted
exactly what the docblock asked for never
delivered a notification.
IcsExporter said it wrote no VALARM. It has written them
since alerts landed.
CLIENT_DEVELOPMENT.md said there was no JMAP calendar API and that
SearchSnippet/get was absent. Both shipped.
CONTRIBUTING said only app:calendar:push registers a push
channel; subscribing has done it since the
subscribe flow learned to.
Three docblocks named getters removed long ago, or described a
method two below the one they sat on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
36bc791f93 |
Measure the JavaScript, and cover the three controllers that had none
Playwright collects V8 coverage when E2E_JS_COVERAGE is set, one raw file
per test, aggregated by bin/js-coverage.mjs. Only this application's own
scripts are counted — Stimulus and Turbo are in the same importmap and
would otherwise dominate a number nobody here can act on.
The aggregation is the part worth reading. V8 nests its ranges: a
function that ran reports one span with a count, and the branches inside
it that did not as zero-count spans within it. So the covered set has to
be resolved per test and only then unioned. Pooling the dead ranges
across tests instead reads "this block did not run in test A" as "this
block never runs" — and since the importmap preloads every controller on
every page, every spec that merely loaded a module cancelled the specs
that exercised it. It reported 28.2% and did not move when three specs
were added, which is what gave it away. Corrected: 63.1%.
Three new spec files, for three controllers with no browser test at all:
search the operator dropdown and what the parser does with an
operator it cannot honour — the difference between "no
results" and "your whole mailbox". 22% to 90%.
admin-queue what a worker is holding, and that the filter asks the
server rather than sifting the page already fetched.
0% to 79%, with app:test:seed-queue to give the panel
something to be about.
filters-scope removing the last condition, and the sentence naming the
account once one is chosen. 18% to 77%.
One bug, found the way browser tests are supposed to find them: the
search box is an <input type="search">, which Chromium clears natively
on Escape — so "first Escape closes the list, second clears the box" had
never worked. The close branch preventDefaults now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a947c4aaac | theming done for now | ||
|
|
3b10b52488 | theme update | ||
|
|
7c8d0de52d | update | ||
|
|
efad90b3de | theme update | ||
|
|
ec29fc7479 | new sweep | ||
|
|
7a1170b643 | update theme | ||
|
|
162fec0944 | theme step1 | ||
|
|
55862ccb2f | Add webapp packages | ||
|
|
db8b7fbac5 | Add initial set of files |