-
Postulo 0.5.0
StableAll checks were successfulCI / Unit tests and coverage (Python 3.14) (push) Successful in 13m50sCI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 1m52sCI / Browser tests (Chromium) (push) Successful in 34m5sCI / Database tests on PostgreSQL (push) Successful in 4m49sCI / Security audit (push) Successful in 2m39sEvery Python / Unit tests (Python 3.12) (push) Successful in 15m40sEvery Python / Unit tests (Python 3.13) (push) Successful in 15m0sRelease / Publish the release (push) Successful in 1m38sDev image / Dev image: build, scan, push (push) Successful in 1h30m58sreleased this
2026-11-01 11:36:19 +00:00 | 110 commits to main since this release🔒 Security
- A photograph uploaded as a document has its location, time and camera cut out by default, without re-encoding the picture: a box on the upload form, ticked unless you untick it. (#964)
- A translation that adds markup the English lacks (a tag, attribute, address or stray <) is left out of the compiled catalogue if a draft and fails the check if reviewed, and the map's "you are here" tooltip is text. (#983)
- The board's fold links encode their hx-vals with as_json instead of pasting a string into JSON, and a template lint refuses any hx-vals or hx-headers built another way. (#985)
- The company edit page no longer prints a translated sentence as markup: the kept-back notice on the company, contact and profile pages is composed in Python with the setting's name in italics, and a lint allows |safe in two templates only. (#982)
- seed_demo takes the new account's password from POSTULO_SEED_PASSWORD or a prompt (--password), never from the command line. (#980)
- Postulo's own JSON log scrubs the same one-time secrets as the access log: password-reset and email-confirmation keys too, from one list. (#979)
- /healthz tells a request a trusted proxy passed on by any forwarding header (not only X-Forwarded-For) from the container's own probe, so it is held to the host list and is not told the version. (#977)
- A request id sent by a trusted proxy is kept when the proxy also sent X-Forwarded-For, judged by the proxy that delivered the request and not by the client address it named. (#976)
- The threat model says the referee rules bind the shipped themes and the sections they are handed, and that a plugin theme is handed full records and trusted. (#965)
- Calendar files (the feeds, the downloads, an interview's file, and what a sync plugin writes) no longer carry your private notes or name the people you meet as attendees, so a calendar program cannot offer to send them updates; the notes' help text says so. (#963)
- The links in a capture notification (mail, webhook, push) are built on POSTULO_PUBLIC_URL when it is set, not on the Host the capture request carried. (#962)
- A logo address fetched on a shared or group-owned company spends the allowance of the person who pressed the button, once, never the owner's or a shared bucket. (#960)
- A GeoJSON file or a page's JSON-LD nested thousands deep is refused, or skipped, instead of ending in a server error. (#959)
- The account archive carries only the exporter's own appointments at a shared or group-owned company, and its count matches the file (#966)
- A company page, a company table and the group count name a parent or child company only to people who can see it, and a company cannot be put under one whose audience could not see it (#957)
- A delete confirmation counts only the reader's own applications, meetings, history and documents, and a reader who cannot delete is neither offered the dialog nor sent its counts (#958)
- The company merge preview names only the viewer's own appointments (#973)
- The threat model says that the contacts a person adds at a shared company stay theirs (#955)
- The share form's lookup of a person by username or address is limited per account (POSTULO_SHARE_LOOKUP_RATE, 20/h), and a hit names the address as typed, never the username it belongs to. (#943)
- Deactivating an account and changing someone's username under People ask for a recent sign-in, like the other People actions. (#944)
- Changing a group's members asks for a recent sign-in and is logged with who was added and who was removed, and Your groups lists every member by username. (#945)
- Giving a listing whose company has a parent to a group shares the parent too, and a refusal is a message instead of a server error. (#946)
- A record cannot be given to a deactivated account, a deactivated account cannot be added to a group or given a plugin access rule, and the groups pages mark the deactivated members who stay. (#947)
- Two shares of one record to one person, or two groups of one name, made at once end as one instead of an error, and a group that is not a number is a 404. (#948)
- A group's row is locked while members are removed, so two removals of its last two members cannot leave records it owns with nobody in it. (#989)
- Free text written through the API (a timeline body, a status note, notes, a description, a letter) is refused over 40,000 characters, as a listing's history is, and a longer timeline entry from a page is cut there with a marker (#953)
- Every answer of the API that says nothing of caching, the iCalendar feeds and all JSON included, is given Cache-Control private, max-age=0, no-store (#952)
- A wrong token sent to openapi.json, to an address the API does not have or with a method it does not take is counted against the address and bars it after sixty, as on every other call (#951)
- Sending a rendering with a token that only holds captures answers with the narrowed capture, as the list does, without the address, company, location or source (#950)
- Renaming a company shared with you counts, against POSTULO_COMPANY_NAME_RATE (20/h), each time it is refused for a name its owner already holds, and a spent allowance refuses every name alike, so a rename is no longer a way to read the owner's other company names (#949)
- Deleting a shared contact with "delete the letters" ticked deletes only your own letters, never another member's, and the count shown is yours. (#956)
- With the data-protection module on, deleting a shared contact leaves the other members the same copy as without it, and the summary counts your rows only. (#961)
- A restore refuses an archive with no signature unless --unverified, since one whose signature was removed looks like one from before signing, and the Restore page shows the verdict and prints the flag (#974)
- An unrecognised POSTULO_BACKUP_DOWNLOAD is refused at start-up and grants nobody, where it used to grant every administrator (#978)
- The monthly dependency workflow resolves, installs and tests the new lock in a job with no secret and hands two files to a second job, in a fresh container, that runs nothing from the new packages, checks what it commits, runs git with hooks off and alone holds the bot's token (#975)
- The PostgreSQL apt signing key is a committed file whose fingerprint a test checks, in the image and in CI, instead of a download trusted on TLS alone (#981)
- The browser CSP test runs under the policy production sends instead of a stale hand copy of it (#984)
- A plugin's settings page now refuses an administrator whose access rule says they may not change its settings, an administrator who is not the operator cannot write or lift a rule about themselves or their groups, and the pages of the mail settings are held to it too (#967)
- An installed plugin can no longer register under the name of one that ships inside Postulo: the install is refused and the built-in keeps the name (#968)
- A wheel's metadata members are read within a size limit, an encrypted member and a wheel of thousands of files are refused (#969)
- The webhook notifier accepts only an https:// receiver, or a plain http:// one on a private network the operator allowed (#970)
- A plugin's templates are searched after every template Postulo and its apps own, so a theme cannot redraw a form widget or a page, as the module always promised (#971)
- The highest catalogue serial seen is kept in the database and not only in the cache, so an old signed index is still refused after the cache is cleared, and the official index tool signs an expiry by default (#972)
- A sync connection's lease is released only if it still holds this run's token (#988)
- A web link saved from a card or an import is refused unless it is a complete http(s) address, whoever hands it over (#991)
- Django 6.1.2, with four security fixes, among them a quadratic header parser reachable without signing in (#986)
- django-allauth 65.20.0 and oauthlib 4: MFA and sign-in flow fixes, a recent sign-in no longer stands in for reauthentication, and the audit no longer ignores any advisory (#987)
- SECURITY.md no longer promises that the audit notices every disclosure: it lags the advisories, so the process now has a weekly manual check of the dependencies' release notes (#990)
- Companies, contacts and listings may be a group's or shared read-only or read/write, under an amended promise: nobody sees another's data unless granted, and a superuser sees any of those three but never anyone's notes, applications or documents (#896)
- On a pull request the CI caches are restored by their exact key and never by a prefix, so a run of somebody else's code cannot read what a run on main restores or leave behind a poisoned fallback (#827)
- The isolation sweep now asks the owner's own record to get past the lookup (and fixes the five entries that never did), checks every download it reaches for no-store, and a ratchet test fails on a new query of an owned model with no for_user or owner (#825)
- The container's entrypoint gives gunicorn the list in POSTULO_TRUSTED_PROXIES, two more forwarding headers are stripped from strangers, and docker/compose.hardened.yml is an opt-in override with a read-only root, no capabilities, no new privileges and memory and process limits (#824)
- htmx is configured with allowEval and allowScriptTags off, and a template lint fails on hx-on, js: prefixes, trigger filters and javascript: addresses (#823)
- base-uri is now none, and a browser's policy-violation reports are written to the log as one line without the address, so a violation in production is seen (#822)
- A new instance derives the browser notifications' signing key from its secret and a random salt kept in the database, so the public key every member is handed cannot be tested against a guess of the secret; an instance that already has subscriptions keeps its key so they go on working (#821)
- Importing a newly installed plugin to see whether it loads happens in a child with an allow-listed environment, and the threat model states plainly that an installed plugin can read every account's rows and stored secrets (#820)
- A wheel downloaded from a catalogue keeps the address's file name only when it is a well-formed wheel name, otherwise a name made here, and a file that cannot be written is a catalogue refusal and not a crash (#819)
- A captured page address with a user name or password is refused, and a board recipe and a posting's source are chosen by the host that is fetched rather than by the whole authority (#818)
- The server's outbound requests no longer honour HTTP_PROXY, HTTPS_PROXY or NO_PROXY unless POSTULO_OUTBOUND_USE_ENV_PROXY is true, since a pinned request through a proxy was verified against the address and not the name (#817)
- The destination guard refuses multicast, reserved and unspecified addresses, which it used to pass as public (#816)
- The threat model and the Hardening page say that OpenID Connect discovery, token and userinfo calls are made by allauth's session and not the guarded client, and why that is acceptable (#815)
- The process that draws a captured page as a PDF is started with an allow-listed environment and in an empty directory of its own, so it does not inherit the secret key or the database credentials (#814)
- A file is handed to nginx or Apache only if it is a PDF, a Word or OpenDocument file, a picture that is not markup or an archive; HTML, XML, scripts, SVG and unknown types are streamed by Django with its own policy headers, and the wiki has the Apache block beside the nginx one (#813)
- A request that declares a body longer than anything its page accepts (25 MB, the backup limit on the backup upload) is answered 413 before the body is read; POSTULO_UPLOAD_CEILING_MB sets it (#812)
- The backup manifest holds a scrypt mark of the field key with its own salt instead of an unsalted SHA-256, so a copy of a backup is no longer a cheap offline test of a guessed key; archives from before still restore (#811)
- POSTULO_BACKUP_DOWNLOAD says whether every administrator, only the operator's own account or nobody may download a backup through the browser, and the threat model names the exception (#810)
- Every name of a hard-linked media file is written into the backup as a plain file with its own bytes, where the second name used to be dropped without a word (#809)
- Importing a kind file creates each application as a draft and moves it to the file's status through change_status, and leaves out the status-change and interview entries a file supplies, so the log says what the fields do (#808)
- An editable table cell drops every field of the page form but the one it edits, so a post carrying an industry to add or a location correction beside the cell changes nothing else (#807)
- A translated sentence that carries a link or a piece of the person's own text is built by htmlstrings.sentence, which escapes the translation and fills only its plain slots, instead of handing the catalogue to format_html as markup (#806)
- The avatar, logo and plugin tiles escape the picture address, the classes and the initials they are given, instead of trusting the caller (#805)
- The messages partial treats a message as HTML only when one of its tags is exactly safe, not when those letters appear inside another tag (#804)
- captures/known answers the similar-by-title matches only to a token that may read, so a captures-only token cannot probe for a listing it did not make (#803)
- Listing remote type, employment type and salary period must be one of the model's choices and a contact's email must be an address, each a 422 otherwise; a refused capture correction no longer repeats the value that was sent (#802)
- With POSTULO_METRICS_ENABLED on and no POSTULO_METRICS_TOKEN, /metrics answers 503 and logs why instead of serving whoever asks (#801)
- /healthz answers the status and the database to anybody, and the exact version only to the container's own probe and to a signed-in administrator (#800)
- Wrong API tokens count against the address that presents them (sixty an hour, then 429), a token without the scope now spends its allowance, and one account's tokens share POSTULO_API_ACCOUNT_RATE (#799)
- X-Request-ID is accepted from a trusted proxy only; from anybody else Postulo mints its own, so a stranger cannot write a chosen id into the log (#798)
- The log says how many orphan files the sweep removed and not which, and a failed capture or logo names its source or company number and not the page's address (#797)
- An account can make the server ask Gravatar five times an hour; the next ask answers that it is a lot of tries and requests nothing (#796)
- The threat model states that administrators can fetch any account's profile picture, as the People page draws them, and members cannot (#795)
- OAuth token requests are no longer redirected: a token endpoint that answers 3xx is a failure, so the client secret and the grant cannot be re-sent to another address (#794)
- A password change, reset or set, an address change and a second-factor change now send a notification to the primary address (ACCOUNT_EMAIL_NOTIFICATIONS) (#793)
- Only the operator's own account can remove another account's superuser flag from People; an ordinary administrator is refused, and both outcomes are logged (#792)
- Gravatar is fetched through the public-only client, like a logo, and the Hardening page lists what POSTULO_CONNECTIONS_ALLOW_PRIVATE widens and what it never does (#537)
- The CSRF cookie is HttpOnly like the session cookie, as the threat model says of every cookie; the script takes the token from the form it sends (#538)
- Restoring an archive or reading a kind file blanks any web address that is not http or https, and the update check keeps a release address only if it is, instead of storing a script address to be drawn as a link (#770)
- A bare number or object where a file expects text, an impossible exported-at or save stamp, and a mail subject with a line break are refused or ignored instead of ending in a 500 (#769)
- A number past what a row can be called, or a foreign-script digit, in a bulk selection, a capture discard, a model choice or a spreadsheet date is dropped or refused as an invalid choice instead of crashing with a 500 (#767)
- The API refuses an id, an offset, an amount or a moment the database cannot hold with a 422 that names the parameter, where it used to answer 500 (offsets and ids to 2^31-1, moments between 1970 and 2100, amounts to twelve digits) (#766)
- A next address that is a bare word, a number or a dotted name falls back instead of crashing the page with a 500: only a path from the root of this site, or an absolute address on this host, is kept (#768)
- The write scope's label and the API's description now say plainly that a write call answers with the whole record, so a token holding write can read every record it can touch (#765)
- Making an API token asks for a recent sign-in, starts on a one-year expiry instead of never, is limited to ten an hour, and the page says how many are live (POSTULO_API_TOKEN_RATE) (#764)
- Replaying an external_id no longer reads a listing's history entry back to a token that may only bind: another writer's text is left out of the answer (#763)
- GET /me no longer tells a token that only holds captures whose account it belongs to; a token that may read still sees the address (#762)
- A token that holds only captures sees the title, dates and review link of what is waiting, not its address, company, location or source, unless it also holds read (#761)
- The API answers a wrong method, and its root, with 401 and no Allow header when there is no live token, so the route table is not probed without one (#760)
- The company form answers a made-up identifier id and another account's the same way, and a test holds it so (#759)
- When a forwarder that is not in POSTULO_TRUSTED_PROXIES has been barred for wrong tokens, the log now says so once an hour and names the setting, since everyone behind it shares one allowance (#757)
- Postulo's own rate limits are counted in a file store beside the data, outside the request's transaction, so a charge survives a view that raises after spending it (POSTULO_RATELIMIT_DIR) (#756)
- The database cache that holds the rate limits and the sign-in lockout keeps 200,000 entries instead of 300, so an outsider cannot push a counter out by making the cache grow (POSTULO_CACHE_MAX_ENTRIES) (#755)
- The shipped gunicorn access log no longer writes the secret in a recovery, invitation, password-reset or confirmation link, or the code in a consent callback (#754)
- Signing out drops the browser's push subscription and unregisters its worker, so reminders stop arriving on a shared browser, and sign-out goes on at once where the browser has no worker (#753)
- A webhook delivery's body is cleared once the receiver has it, finished rows are deleted after a week, and deleting a connection deletes its deliveries (#752)
- Expired sessions, and the imports they were holding for review, are deleted by the scheduler's pass and by the hourly tidy-up that pages ask for (#751)
- Deleting an account also deletes the invitation it accepted, which carried its address and an administrator's note, and the sessions it was signed in on, with the imports they held (#750)
- The Django admin's profile inline shows only the preferences that make an account usable, not birth data, nationality, gender or anything else that identifies a person (#749)
- The OAuth consent state carries a nonce held in the session and spent on use, so a captured state cannot be replayed with another person's code; the connection is looked up among the signed-in person's own (#748)
- A wildcard in POSTULO_ALLOWED_HOSTS is refused at start-up in production, and the links and the name in a message come from POSTULO_PUBLIC_URL when it is set, not from the Host header (#747)
- A next address on an https instance must be https, whichever page takes it: sign-in, sign-out, sign-up and single sign-on follow the same rule as the rest of the interface (#746)
- An account can try only so many usernames in an hour (POSTULO_USERNAME_RATE, 10/h), since naming a taken one says it is taken (#745)
- A sign-in lock no longer shows which names belong to one account: the account's count refuses in the ordinary words, and only the typed name's own count says too many attempts (#744)
- Authenticator secrets and recovery-code seeds are encrypted in the database under the instance's field key, and the ones already stored are encrypted by a migration (#743)
- An API token no longer survives the password it was made under: changing, resetting or recovering the password, or a single-sign-on link wiping it, ends the account's active tokens (#742)
- An account that signs in only through the identity provider must have done so within the last few minutes before deleting an account, taking a backup or making a recovery link, since allauth lets it through unasked (#741)
- Making a recovery link, appointing or demoting an administrator and deleting an account from Server settings ask for a recent sign-in, as the backups do (#740)
- A recovery link is revoked when the person it was made for signs in or changes their password themselves, instead of staying usable for its whole hour (#739)
- Two sign-ups that overlap on PostgreSQL can no longer both become administrator: the check is taken under a lock on the settings row, and the event is logged (#738)
- Single sign-on is started by a POST with the CSRF token behind a page that asks, not by a link on any page, and the content security policy's form-action names the provider's origin (#737)
- Single sign-on no longer spends an invitation addressed to somebody else: the provider's verified address has to be the one it names (#736)
- The archive import charges its phone-number lookups to the allowance, and once it is spent skips free and held numbers alike in one line (#890)
- Backup archives are signed with a MAC under the instance's key, with a digest of every media and plugin file, and a restore refuses one that does not check out, or restores it without its plugins directory, unless told otherwise (#772)
- Sync now is limited per account (POSTULO_CONNECTION_SYNC_RATE, 20 an hour), as Test is (#786)
- The built-in local store can no longer be connected by hand through its create address, and a connection to it never gets copies (#785)
- A plugin's .pth files are never run at start-up and a wheel that ships one is refused, so switching a plugin off stops its code (#784)
- A signed catalogue index may carry a rising serial and an expiry, and an index past its expiry or older than one already seen is refused, so an old signed index cannot be replayed (#783)
- The GeoNames download and the update check are read through a bounded GET: at most 64 MiB, a table of at most 512 MiB unpacked, and a deadline (#782)
- A webhook, a push service, a link check and a name lookup are held to a total time, never read an answer they do not use and never inflate one, so a hostile receiver cannot stall a worker or the scheduler (#781)
- What Postulo fetches from strangers (a posting, a link check, a logo) dials the web's ports only, 80, 443, 8080 and 8443 by default (#780)
- The capture parser limits the tree to 512 levels, so a page of thousands of unclosed tags no longer costs minutes of CPU (#778)
- A company or plugin logo is decoded as PNG, JPEG, GIF, WebP or ICO only, not as any of the forty formats Pillow reads, and Pillow's pixel guard is put back after use (#777)
- Rendered-document and export downloads check the owner's folder as uploads do, and the check compares paths and not strings (#773)
- A downloaded file's media type comes from its extension alone, so a title that reads like a
data:URL cannot steer the Content-Type (#771) - A CV entry cannot be saved pointing at another person's entry or CV, whoever writes it (#774)
- Pages drawn for a signed-in person carry
Cache-Control: private, no-store, so Back after sign-out no longer restores them from the browser's history cache (#787) - Every workflow checks the uv installer's sha256 before running it, and the Dockerfile copies uv from an image named by digest, so a moved tag or a changed script can no longer alter the tool that builds a release. (#790)
- An unverified address claim is dated and expires after the confirmation link's three days, gives way to its real holder's sign-up, and cannot be added without the password or where the several-addresses feature is off. (#891)
- The PostgreSQL compose profile still hands its database container the whole .env, secret keys included; this is now stated in the threat model, the compose file, .env.example and the wiki, with how to close it. (#791)
- On a bare-metal install the database directory, uploaded documents and the log are created readable by their owner alone (0700/0600) instead of by every account on the machine; the image is unchanged. (#789)
- Anything sent as a download (the calendar file, the report as CSV and PDF, a contact's data-subject export) now carries Cache-Control: private, max-age=0, no-store, so no cache keeps it. (#788)
- An invitation bound to an email address no longer marks that address verified at sign-up, since Postulo never mails the invitation: the address is confirmed by the usual link, and the invitation page says so. (#732)
- Single sign-on matches the provider's address only to an account that has verified it, so a member who listed somebody else's address can no longer have them signed in as that member. (#730)
- A password reset key or a sign-in code is mailed only to an address its account has verified, so a member can no longer claim a stranger's unused address and receive its holder inside their own account. (#731)
- The JSON log, the Logs page and the log collector no longer receive the query string of a request that ended in a 4xx, and the token in an invitation or recovery link is replaced by ; lines already written keep the old form until they rotate away. (#733)
- A candidate file's review no longer says which telephone numbers other accounts hold once the number allowance is spent: every number is then refused in the same words, as the form does. (#735)
- Rate limits longer than five minutes (number collisions, capture, fetch, the API, texts, the wrong-token bar) now last their whole window on the stock database cache instead of expiring five minutes after their last use, so the allowances are no longer about twelve times what is set. (#734)
- Every API list now refuses a limit above 200 with a 422 and declares the bound in the OpenAPI schema, so one call can no longer make a whole account the page while holding the database's write lock. (#434)
- Importing an archive reads every file, and the manifest, through a size cap, so an entry that unpacks to gigabytes is left out and reported instead of exhausting memory, and an imported avatar or company logo goes through the same checks as an uploaded one (an SVG is sanitised, a picture re-encoded). (#466)
- Check, Check all, a new logo address and Find logo now spend a fetch allowance,
POSTULO_FETCH_RATE(300/h by default, once per address fetched): Check all stops when it runs out and says how many links it did not check. (#407) - A release tag is now refused unless the PostgreSQL job and the security audit passed on its commit, as well as the test legs, browser and checks; maintainers tagging with either red are blocked. (#419)
- Isolation tests now cover administrator-only routes, ids sent in API bodies, form choices and Your details rows, and the API download of a sent document, so a dropped ownership or staff check fails the suite (#422)
🔧 Changed
- CI runs the unit tests on three workers: a test worker now lets go of the catalogues, URL tables and CLDR locales of every language it met after each file that met more than four, and one worker takes the translation tests, so a worker stays far below the 1 GB it reached. (#1033)
- CI keeps the compiled translations between runs, installs Chromium's libraries in one apt run, clones history without old files for Checks, names the heavy tests' file, and the test settings fetch no map tiles. (#1033)
- A push to an issue branch no longer runs CI by itself: its pull request runs it, on every push, so open one early, as a draft if the work is unfinished. Only main and release branches run CI on a push, and a newer push to the same pull request cancels the run it supersedes, never on main. (#1033)
- The importer that reads Europass files is called Europass import, on the plugins page and in Your career's Import menu, so it is no longer mistaken for the Europass CV theme from Postulo Templates; its internal name stays europass, so saved switches keep working. (#911)
- Every change reaches main through a pull request: a branch per issue, Closes #N, squash-merged once CI is green (Weblate's with a merge commit); CONTRIBUTING.md, AGENTS.md and the release steps say so. (#940)
messages.py checkcounts, per language, the drafts it leaves out of the compiled catalogues and fails when one has more than src/postulo/locale/excluded-drafts.txt records (ur 51, sw 19, id 1 today), Weblate's pull requests included; a fall asks for the number to be lowered. (#910)- On a phone the navigation bar at the foot of the window holds every item, in your order, and no More: it keeps one row and scrolls sideways when they do not fit, fades out the end that has more past it, and keeps the page you are on and the link you tab to in view. (#912)
- A language menu shows a language's variants under its own name (Português: Portugal, Brasil; 中文 by script), its search box finds a language by its English name too, and a browser asking for plain pt, es, fr, en or zh gets pt-BR, es-MX, fr-FR, en-GB or zh-Hans. (#1053)
- Lines without spaces break where a reader expects: Chinese and Japanese take the strict kinsoku set, Korean keeps its words whole (word-break: keep-all), and the image names libthai0, which Pango breaks Thai between words with. (#1052)
- Urdu is set in Nastaliq: the interface and the CV, letter and portfolio themes ask for Noto Nastaliq Urdu first with a taller line, Arabic keeps Naskh, and check_fonts and the server overview say whether Arabic (Nastaliq) is drawn. (#1054)
- On a phone the Applications board stacks its columns down the page, full width, with folded ones as one-line headers that open in place; the page scrolls instead of a box, and column names stick under the header. (#844)
- The setting "Underline the page you are on" is now "Underline links in navigation": turned off, it also stops the board's column names underlining under the pointer. Card titles keep their underline. (#843)
- A company's names in other languages are read from a popover opened by a languages icon beside its name, drawn only when there are some; adding, fetching and removing them moved to its Edit page. (#917)
- On a phone the companies' map shows the Table link as its icon alone, with a "Table view" tooltip below it; the word is kept for screen readers and shown from the small breakpoint up. (#916)
- On the companies' map, the Zoom in, Zoom out and Near me tooltips appear beside their button, towards the map, instead of over the neighbouring button; tooltips can now ask for a side (
data-tooltip-place). (#915) - Your details has one help pattern: a question mark after the heading and at the top right of every section, with a tooltip and the full text in a drawer, and no helper text under any heading. (#918)
- Form of address, pronouns and gender are stored as language-neutral concepts (
he,doctor_f) and drawn in the language of the page or document; only "Other" stays free text. The profile API returns{key, label}for each. (#921) - Telephone numbers, postal addresses and social profiles work the same for a company, a contact and Your details: the star, the bin that removes a row at once and the help mark on all three, a browser fills a phone number only on your own, and a contact's card on its company's page lists its addresses. (#934)
- Form of address, pronouns and gender are stored as language-neutral concepts (
he,doctor_f) and drawn in the language of the page or document; only "Other" stays free text. The profile API returns{key, label}for each. (#921) - The instructions for AI assistants moved from CLAUDE.md to AGENTS.md, which Codex, Copilot, Cursor and others read; CLAUDE.md only imports it (#939)
- The section menu of Your details has an icon on every entry, and Your CV photo is in it. (#920)
- A social profile is one to a service: a service another row uses is greyed out in the menu and a second one is refused, except Other and services on many servers such as Mastodon; rows that already share one are kept and flagged. (#925)
- An identifier can be marked primary with a star at the start of its row; a CV prints it first. (#922)
- An address is shown on the map by a pin beside its star and bin on Your details, which asks before it is turned on. (#923)
- Clicking the fixed front of a username's address puts the cursor in the name. (#924)
- Every service in the social-profile and code-repository selects has a one-colour brand mark, black on the light page and white on the dark one, from Simple Icons and, for LinkedIn, from Font Awesome Free (CC BY 4.0) (#926)
- On Your details the date of birth's calendar button stays beside the day, month and year boxes on one line down to 320px, and its picker is Postulo's own calendar (month and year menus, a keyboard-driven grid of days, named by your browser's language) instead of the browser's date input. (#928)
- The companies map puts its zoom and Near me buttons, with tooltips, above a compact footer line in the bottom-right corner, its heading without a card or count, the Table button top right and the list of places under the heading (#909)
- The companies map draws its zoom and Near me buttons at the bottom of the start side, above the footer's line, and on the right on a right-to-left page (#907)
- Companies are classified by sector, the NACE section, above their industries: the company form narrows by it, and the table, industries page, report and insights group by it; ESCO's NACE links order the suggestions (#897)
- The companies map credits its tiles and Leaflet at the end of the footer's line over the map, in the same look, instead of in a second box of Leaflet's own (#908)
- The calendar's kinds of entry, its key and filter, come after the month, week, day or agenda rather than above it (#903)
- The companies' map draws a marker for every address, gathered into clusters that count companies; places are grouped by where they resolve to, so two spellings of a town are one, and the list names each address's kind and says when a company is counted at several places (#703)
- A company's Location is the town and country of its primary postal address: each location becomes a Work address on upgrade, keeping its point, the form's Location and correction boxes go, and the API, CSV and kind files still take a location line (archive format 68) (#702)
- Only a person's own telephone number is unique across the instance: a number recorded on a contact or a company is unique within it, asks nothing and spends no allowance, and an address is unique per holder rather than per account (#329)
- The companies' map fills the window, with the heading, a Table button, the list of places and the footer's line drawn over it, darker tiles in the dark theme, and the table's Map button carries the map icon (#849)
- The companies' map is drawn by Leaflet over OpenStreetMap's tiles by default; Server settings → Plugins → Maps (or POSTULO_MAP_TILES) chooses a tile server of your own or no tiles, and only the map page's security policy opens to the tile server (#898)
- Who may use a plugin or module, switch it for themselves and change its settings is a list of rules for groups and accounts, kept on a page of its own with a Groups page under People; the checkbox Each person may change it is replaced by Everybody may switch it on that page (#886)
- Browser, Email and Webhooks are the sub-modules of a new module, Built-in notifications, and are named Browser notifications, Email notifications and Webhooks, each with its icon (#876)
- The six features that let a field hold more than one value are the sub-modules of a new module, Multiple fields, and are named Multiple email addresses, telephone numbers, code repositories, social profiles, websites and messaging contacts, each with its icon (#875)
- The wiki, CONTRIBUTING, PLAN and the CI header describe the project as it is now, and checks keep the settings pages, the container name and the CI header right (#406)
- The plugin author's page and the logo docstrings describe the code: all seven notifier events, a logo as a file name, the real size limits and every shipped plugin, with checks that keep them so (#405)
- The wiki's Configuration page now says an unreadable rate stops start-up, that implicit TLS is offered and names every rate setting, and two checks keep it so (#404)
- The masthead search narrows the Documents lists and the Calendar first, with Search everything beside it; on the Calendar it narrows the period on screen (#848)
- The section menu of Your details and Your career has the look of vertical tabs, still anchors to sections of one page (#870)
- The calendar's view switch sits beside the period, aligned to the end, and the heading's row keeps Import, Export and the new-entry buttons (#861)
- The calendar's week view is one card of seven days divided by borders, with no gaps between them (#860)
- The calendar's Earlier, Today and Later are a button group of Previous, This month/week/day and Next, and the period opens a date picker (#858)
- A company's logo is changed from a dialog on its edit page, with its own form for an address or an upload; the company form now draws its parent (#853)
- Industries are drawn as coloured badges with an icon and a tone taken from their NACE section, on the company page and in the companies table (#856)
- Listings: Columns and Views move into the filter row, at its end, and are drawn only while a table is (#840)
- The Listings state strip is drawn like Basecoat's tabs (a tab-nav component of links, not an ARIA tablist) (#842)
- Documents, Calendar and Your career have a question mark beside the heading: the sentence on hover and the help in a drawer (#847)
- "Companies on a map" is now "Maps", credits GeoNames, the Unicode CLDR and Natural Earth in its description and on the map page, and has the map icon (#882)
- The module "Employers with a structure" is now "Companies with a structure", with the pyramid icon added for its logo (#881)
- POSTULO_CONNECTIONS_ALLOW_PRIVATE is documented as one switch for the whole instance that lets every account probe the private network through the Test buttons (#779)
- Nationalities are named as nationalities (Portuguese, portugaise, portuguesa), not as countries, on a CV and in the menus of Your details, in English, French and Portuguese (#865)
- The gender list offers Female, Male and Non-binary (Féminin, Masculin; Feminino, Masculino), and a profile, an archive or a candidate file that holds an old word is carried over to the new one (#864)
- Your details is a sidebar and two columns of cards from 2xl, filled row by row, each no wider than before (#871)
- Your career takes the whole screen like Your details, its sections keeping the measure of prose (#872)
- The sidebar's label on Your details and Your career names the page, larger, once its heading has scrolled behind the masthead, in place of the section's title (#869)
- Social-profile and repository rows draw the service's own brand mark where Postulo ships one, in the row and in the list of services (#867)
- A contact row reads star, type, boxes, bin on one line, and a postal address carries its star and bin on the line of its type (#866)
- A board card's status is changed from a pencil menu at its corner (a row per status, the current one ticked) instead of a select at the foot of the card; dragging and the keyboard keep working. (#845)
- A company's identifiers are removed with a bin that asks first and takes the row off at once, as on Your details, instead of a Remove tick box applied on Save. (#857)
- Delete on a company, a listing or an application asks in a dialog over its own page, with the same counted consequences, instead of on a page of its own; the page still answers at its address. (#854)
- On phones the table searches (Listings, Applications, Companies) open in the masthead row like the page search, with the + menu hidden meanwhile and 16px text; the bar under the masthead stays for tablets and without scripts. (#833)
- The account menu is a quarter wider, and a long address is cut short with an ellipsis (whole in the page and in a tooltip) instead of wrapping. (#834)
- The footer's line is the administrator's motto (a new field under Server settings → Defaults, starting as the tagline it showed), defaulting to “your job search, your data”, and is left out on phones so the footer starts with “Powered by Postulo”. (#835)
- The footer's “Powered by Postulo” no longer shows the version and links to the releases page for everybody, signed in or not. (#836)
- Single sign-on no longer signs somebody into the account holding the same address unless POSTULO_OIDC_LINK_BY_EMAIL=true is set; an instance with a provider and the variable unset is warned at start-up (postulo.W002), and those people connect the provider once from their own account. (#892)
- The skills section's example heading is now “Programming languages”, so “Languages” means the spoken ones wherever Postulo writes it; the demo data follows, and nothing in anybody's record changes. (#690)
- CI runs the four-times-size Markdown worst cases in a step of their own, one process, after the unit suite: beside it they pushed the job over its memory limit. (#830)
- Translations are made in Weblate only:
docs/TRANSLATING.md, the contributor guide and the assistant's instructions say how and what a commit may do with a string, and no test pins a translated word any more. (#706) - The catalogues are written in Weblate's layout, so what it saves passes CI unchanged: a draft is flagged
fuzzyand still compiled, references name a file, the tool sets no header date, andcompilewriteslocale/status.jsoninstead of it being committed. (#349) - A test now fails, naming the section and the list, when a career section is missing from the archive, candidate file, CV headings, importer or translation lists, so a new section cannot be half added (#692)
- CI no longer carries a
permissions:block that Forgejo 16 ignores and warns about on every job; the workflow audit is told why forci.ymlalone, and the write grants of the release and image workflows are still to be given to the instance as Authorized Integrations (#727) - Deleting a listing now deletes the captures that became it, with their kept pages; a capture nobody saved can be deleted at once, and
manage.py prune_capturesfinds saved captures already stranded without a listing (#664) - A profile picture, its Gravatar copy and a company logo are now rows in the database that go with their owner however it is deleted (page, merge, admin, account deletion), no longer files that could be left behind (#662)
- The test suite now runs views inside the per-request transaction production uses, so a view that forgets non_atomic_requests fails in tests too. (#572)
- The applications and companies tables no longer de-duplicate every row on each view, so counts and pages load faster, most on large accounts. (#552)
- The company page's duplicate check is much faster for people with hundreds of companies: legal forms are looked up by their edge word and company names are cached. (#553)
- Social profile rows now ask for the username with the service's address shown fixed in front of it, and a name that breaks the service's rule is refused beside the box (#678)
- Find logo and Refresh logo open a dialog on the company's page instead of a page of their own (#674)
- Companies in Employment activities and public employment services are now offered first as the agency an application went through, with a "Show every company" link for the rest; the employment-service company type is now labelled "Public employment service" (#671)
- Removed a set of unused code, including the orphaned posting form at /jobs/postings/new/, so Listings is the only place to add a listing (#401)
- CI starts the browser tests first, checks lint, migrations, catalogues and the production settings once instead of on every Python, bounds each test step's time and lists each suite's slowest tests; a release also needs those checks to pass. (#712)
- CI keeps the ESCO classification between runs and downloads it live only on its weekly run. (#714)
- CI's browser tests no longer trace every test: a failed test is run again alone with a trace, and the job stays failed either way. (#715)
- CI runs the unit tests on Python 3.14 for every push; 3.12 and 3.13 run in a new Every Python workflow weekly, on release branches and by hand, and a release needs all three. (#713)
- The dev image is built only from a commit CI passed: its workflow waits for the commit's CI before building. (#716)
- A signed-in person whose profile language is blank is now read in the instance's default language, never the browser's, and the menu's blank choice says Use the instance default. (#398)
- Language and time and Server settings → Defaults say which language and which time zone the blank choice stands for (Deutsch de — Default, Europe/Paris — Default), with the flag and the name in its own language where the control can draw rows, and "In force now" names the language instead of printing a code. (#698)
- The browser tests draw every page in their own copy of DejaVu Sans on every machine, so a layout that passes locally passes on CI. (#717)
- Four browser tests wait for what they read instead of a fixed time or an idle network. (#718)
- Three unit tests no longer depend on the machine being idle: robots.txt, the two-factor codes and a slow-filter guard. (#719)
- Browser tests sign in through one helper that hands the browser a session, instead of filling the sign-in form in every test. (#720)
- CI runs the browser tests on two processes, the walks of every page first. (#721)
scripts/messages.py statsfinds the catalogue sets once instead of once per language: 4 seconds instead of 45. (#724)- The unit suite no longer repeats the catalogue and collectstatic checks CI's Checks job makes;
pytest -m stepruns them. (#725) - Browser tests wait for the page's state instead of a fixed time where the time is not the point, and every fixed wait left says what it measures. (#723)
- Server settings Overview no longer walks the whole media directory on every visit; the file count and size are cached for five minutes, so the page opens fast on older instances. (#488)
- GET /api/v1/documents now cuts its page in the database and never reads a sent document's text, so a page costs the page and not every file ever sent. (#551)
- CONTRIBUTING.md says what belongs in the browser suite and how to keep it fast. (#726)
- The browser suite's walks of every page check the accessibility, target size, text spacing and described-by rules on one load per page, in three walks instead of six. (#722)
- Pages no longer re-read the package metadata for the footer's version, and the dashboard draws its widgets without re-running every context processor for each one, so it renders faster (#560)
- Plugin policy no longer has a "switched off for the instance" step that compared a plugin's name with package names and never matched; the registry's filter on disabled packages is the instance-wide switch, as before. (#596)
- Address rules no longer claim to print an address in its country's order, which nothing did; the order only lays out the form's lines, and the unused print and expects functions are gone. (#643)
✨ Added
-
A European Digital Credential (the JSON-LD file the Europass wallet gives you, sealed or not) can be uploaded as a certificate, diploma or diploma supplement and is kept byte for byte, labelled "Seal not checked by Postulo" on the Files page and in the API. (#670)
-
A European Digital Credential's edit page shows what it states: its title, who awarded it, when, and its EQF level where it gives one. Read from the file by Europass import each time, nothing fetched and nothing personal read or kept. (#670)
-
A plugin can make an interview with
schedule_interview, refused exactly as the form and the API are (the kind, the end, the people now checked in the service), and read a person's deadlines, closing dates and other dated things in their own time zone withcalendar_items. (#330) -
Export a table whole: Applications, Companies and Listings each have Export, which writes every row in your columns, filters and sort as a PDF set by a theme (a new list kind, offered and remembered like the report's; landscape by default) or as a CSV of the same rows, downloaded and never filed (#341)
-
A connection's form can offer what is on the other service: a plugin's
discoverlists what it finds when you press Look on the server, and you choose several, each in a mode the plugin offers (read, write), from a table that keeps one that vanished in sight until you let it go (#332) -
Print the calendar: Print, beside the views, turns the month, week, day or agenda on screen into a PDF set by a theme, every entry and none of the buttons, on the paper chosen (a month on its side by default); a new calendar kind for themes; a download, never filed (#342)
-
A Pairing page for each sync connection whose plugin lists its items: pair an item with one of your records, have the plugin make or fill one, unpair or pair again; each paired record says so, and asking the server has its own allowance, POSTULO_CONNECTION_PAIRING_RATE (#327)
-
Choose the paper: A4 or US Letter, portrait or landscape, set for the instance under Server settings → Defaults, for yourself under Settings → Export, and per export; a theme declares the papers it prints on (none is A4 portrait only), and Chromium, WeasyPrint, Word and OpenDocument honour it (#344)
-
Every capture on the API carries a
posting_key, the same for two captures of one posting and different for two, so a client counts the postings waiting for review without copyingsame_url(); a keyed hash told to a captures-only token too, and answered per address by/captures/known(#711) -
On Your details, a card holding a change not yet saved is ringed in soft amber, says "Not saved" beside its title, and has a dot on its entry in the section menu; undoing the change takes the mark off, and a refused save marks again the cards it brought back changed. (#927)
-
A theme can set the job-search report: a plugin may offer one (ThemeKind.REPORT), the report's page offers the theme and the language it is drawn in and remembers the theme, plain draws it as before, and a filed document records which theme drew it. (#340)
-
For sync plugins, a
SyncLinknames its twin's collection and a slot, so one record can have several twins; one remote item pairs with one record (bindraisesAlreadyPaired),bindclearsremote_gone,unbindkeeps the row marked unpaired, andfor_remotefinds a link by collection and UID. (#326) -
Reminders are on the plugin surface with one service each to make, change, move, tick off and untick one, each naming who did it; a reminder changed by a token or a plugin says so beside it, and a done one can be marked Not done on the calendar and through
POST /reminders/{id}/reopen. (#331) -
A map pin whose holder has a picture is drawn as it: the company's logo, or your own picture on your own addresses, ringed in the brand's colour; a contact keeps the dot, and a picture that fails to load gives its pin the dot back. (#914)
-
A plugin theme can carry its own fonts, SVG drawings and images: {% theme_asset %} writes them into the document before drawing, so nothing is fetched; the preview alone allows a data: font, and check_fonts and Overview say which scripts a theme's font draws. (#343)
-
A backup destination plugin kind (
postulo.backups): chosen under Server settings → Backups, it receives each verified archive encrypted to an age recipient, retried by the scheduler, with remote retention and its own metrics; a waiting archive is never rotated here. (#325) -
An address with a street can be pinned at its street by Nominatim, Photon or addok, off by default and switched on with the operator's consent; the scheduler looks one address up a second, existing ones included, and never moves a pin placed by hand. (#913)
-
With Postulo installed, a Postulo link followed from mail, chat or an extension opens in the app where the browser captures links, and reuses an open app window, taken to the link's address; the browser suite asks the full Chromium whether it would install Postulo, signed in and signed out. (#899)
-
A publication can be filled in from its DOI (Look up, via doi.org) and suggested from your stored ORCID record, only when you press: blanks only, nothing kept until you save, 20 presses an hour (POSTULO_LOOKUP_RATE), and a publication-lookups feature to switch it off. (#688)
-
Your career gains Citations: how often your work has been cited, typed with its source, a required date (proposed as today) and optionally one of your publications; a CV prints "Cited 1,240 times (Scopus, September 2026)", after the paper's title, and the archive and candidate file carry them. (#697)
-
Register lookup, a plugin off until an administrator switches it on: Look up beside a company's register number (France, Norway, Finland, Czechia, Poland) or LEI (GLEIF) offers the register's name, address, state and NACE activity, keeping only what you tick, and checks an LEI against the number. (#1021)
-
Settings → Export sets what every exported PDF, Word and OpenDocument file says about itself: each of title, author, subject, keywords and language from the document, fixed or left out, and Strip all metadata, which keeps the language and takes WeasyPrint's name out of a PDF (#878)
-
The languages each release must carry complete are listed once, in
messages_tool.SELECTED: 0.5 adds fr-CA, en-CA, es-MX, be, cnr, fo, nn, rm, az, kk, zh-Hans, zh-Hant, Arabic, Hindi, Urdu, Bengali and Indonesian to 0.4's 39, and every release gate reads it. (#347) -
The plugin surface holds companies, departments and the person's own card (read only, with the sign-in address), a holder's numbers, addresses and links as plain values, and services to update a company, make a department and a contact, and write several numbers, addresses and links; each takes an
actor. (#328) -
A company register number is checked against its country's check digit (France, Czechia, Norway, Finland, Estonia, Poland) and links to the register's own page (seven countries) or the EU business-register search, all offline. (#935)
-
Date pickers of Postulo's own beside every date box, and on the calendar one per view: months, a week, a day, and a range for the agenda, which now takes an end day (
until=) (#933) -
A company has a status: its legal state (active, in administration, acquired, dissolved) and dated hiring signals, shown by its name, in the table and on the map, and as a notice on its listings. (#941)
-
A company's vCard carries its mailbox, every number and address, its website and its social profiles, and importing one reads them back; the API takes and returns
phone_numbers,postal_addressesandweb_linkson a contact as on a company. (#934) -
A code of conduct, the Contributor Covenant 2.1, for issues, pull requests and Weblate, with reports to the maintainer's address (#1007)
-
An accessibility statement, ACCESSIBILITY.md: WCAG 2.2 AA as the aim, how it is checked, the known limits, and how to report a barrier without an account (#1008)
-
read_body,deadline_in,untilandBodyRefusedare onpostulo.plugins.api, so a plugin can bound what a remote server sends it (#967) -
Groups own companies, contacts and listings, records are shared with people or groups, each person's notes and listing decisions stay their own, and leaving keeps a copy of what is yours; a conflicts page settles duplicates across owners (#896)
-
Seven more European languages are scaffolded with empty catalogues for 0.5.0: Azerbaijani, Belarusian, Kazakh, Montenegrin, Norwegian Nynorsk, Romansh and Faroese; none is offered until a string is translated (#269)
-
A company holds social profiles, its LinkedIn page and its accounts, as a contact does: on its form, on its page with each service's mark, in the API, in the archive and through a merge (#906)
-
The calendar's Month, Week, Day and Agenda buttons each carry an icon, drawn alone on a small screen (#904)
-
The companies' places export as GeoJSON, and a GeoJSON file's points, matched to companies by name or Wikidata identifier and ticked, place their addresses by hand; a GeoJSON sub-module of file formats that needs Maps (#880)
-
Fetch from Wikidata also reads where the company is, and keeping it places its primary address there, unless a person placed that address by hand; taking it off places it from its words again (#701)
-
A company keeps its names in other languages, fetched from its Wikidata item on a press or typed where Wikidata has none; a letter in that language prints it, and search, the API, a merge and the archive (format 69) carry it (#673)
-
Postal addresses are placed on the map offline, at their postcode where the GeoNames postal codes were downloaded (fetch_geonames --postcodes) or at their town, or by hand on a page of their own; a contact's and your own are drawn only when you tick Show on the map (#702)
-
A company keeps its general email address, its telephone numbers and its postal addresses, on its form and page, in the API, an export and a merge, and a contact's form edits its addresses (#329)
-
Near me on the companies' map moves it to where you are, asking the browser only when pressed and sending the position nowhere; every other page forbids asking (#850)
-
The hello-world plugin is at 0.3.1 in the official catalogue, with its notifier under the name helloworld-notifier so Postulo no longer leaves it out (#593)
-
Postulo installs as an app on Chromium browsers and Safari: a completed manifest with a maskable icon and shortcuts, title-bar colours for both themes, Install in the account menu, and an offline page, the one thing the worker keeps (#895)
-
The official catalogue lists Postulo Templates 0.3.0, the curated document themes, beside the hello-world plugin (#729)
-
Server settings -> Plugins draws an installed package that registers several plugins as a parent row whose switch is its sub-plugins' master, and catalogue listings carry categories shown as badges and filterable (#883)
-
Server settings -> Plugins draws a Settings button under the switch of every plugin or module that names a settings page in its manifest, hidden when it has none and shown disabled, with the reason as its tooltip, to someone who may not change them (#885)
-
A module, Extend file formats, holds PDF (always on), DOCX, ODT, iCalendar, vCard and Europass: one that is off is not offered and its download, import and feed answer 404; the text of Word and OpenDocument files you upload is kept for search (#877)
-
A plugin or catalogue release can name the plugins and modules it needs; one whose need is missing or off is off with the reason beside it, an install that lacks one is refused naming it and offering it from the same catalogue, and a plugin others need cannot be removed (#884)
-
A module can name a Lucide icon in its manifest, drawn where a logo goes; Companies with a structure wears the pyramid and Maps the map (#874)
-
A module may have sub-modules, each with exactly one parent, and categories; switching a parent off turns its sub-modules off and takes their switches away, and switching it on brings them all back (#873)
-
The breadcrumb can be a thin line that opens while it is pointed at or focused and closes on Escape; a touch screen draws it whole (#832)
-
An optional breadcrumb under the navigation bar, sticky with it, switched on under Accessibility; the company, listing and application pages fill it (#831)
-
The date of birth has a calendar beside its day, month and year boxes, which stay as the form and take a year alone (#863)
-
The calendar has three settings: words or only chevrons on its period buttons and abbreviated or full weekday names (Appearance), and the first day of the week (Language and time), which the week view, the month grid and the reports all follow (#859)
-
Import and Export are a button group beside the heading of the Calendar, Your details and Your career, from a registry of formats a plugin can add to (#862)
-
Appearance has a setting for buttons that carry an icon and words: both, icons only (the words become a tooltip) or labels only (#852)
-
A company's page groups Edit, Merge, Export and Delete with icons, and Export offers the company as a vCard or as JSON, with or without your notes (#851)
-
A plugin can place a dated calendar entry that belongs to no application (add_reminder, idempotent by uid, never unticks) and end it (retire_reminder); removing the plugin retires its entries (#887)
-
A connected plugin may offer up to six https links on its connection's card (an optional links method), drawn as external links (#889)
-
A company's identifiers are badges with the scheme's mark or icon, and a setting chooses icon and label, icon only or label only (#855)
-
Appointments with an adviser at a public employment service, typed by hand: on the calendar, in the diary feed, in the report, in the archive and on the plugin surface (#888)
-
A question mark beside the Listings heading shows the page's summary on hover and opens its help in a drawer, replacing the helper paragraph; page titles can take a help topic. (#839)
-
“Capture a posting” and “Add a listing” on the Listings page are one button group with icons; the capture buttons elsewhere take the icon too. (#841)
-
The footer's “Help” link has a book icon beside it, and only the icon on phones (the word stays for screen readers and as a tooltip). (#838)
-
The footer's “Source code” link carries the Forgejo mark where the address is on a Forgejo host, and only the mark on phones (the words stay for screen readers and as a tooltip). (#837)
-
scripts/messages.py guardrefuses, in CI and before a commit, any translation written outside Weblate,gateholds Weblate's pull requests to complete French and Portuguese, andscripts/weblate.pylocks, syncs and checks the Weblate project. (#706) -
The official plugin catalogue is published, signed, with postulo-helloworld in it: Server settings → Plugins → Where plugins may come from has an official row that is off until an administrator switches it on, and a plugin whose file it signed is labelled Official. (#729)
-
Nine languages are scaffolded with empty catalogues for 0.5.0: Chinese (simplified and traditional), Hindi, Urdu, Bengali, Indonesian, Canadian English, Canadian French and Mexican Spanish; none is offered until a string is translated, and the image now carries a CJK font (#728)
-
Diploma and Diploma supplement are kinds of file, and a certificate, diploma or supplement can say which education or certification entry it proves; the Files list, the career page, the API and the archive carry the link (#669)
-
Uploads are now checked for what they are, kept with a size and checksum the archive carries (format 54), scrubbed on a schedule without ever deleting a damaged file, and orphans are swept after a grace period (#663)
-
A CV can print a photograph of its own, kept apart from your picture, uncropped and off until a CV's "Print your CV photo" is ticked; it is left out of the plain text and Word file, and the archive format is now 55 (#668)
-
Your career gains a Driving licences section: tick the categories you hold (AM to DE) and a CV prints "Driving licence: B, A2 (Portugal)"; the licence number and everything else on the card is never kept (#691)
-
A certification's issuer is linked to one of your companies (added if new, with no industry), offered with awarding bodies first, with a notice when it has no NACE industry; archive 52, candidate file 14 (#686)
-
An education entry now links its institution to a company of yours, offering places of learning first and adding the Education industry to a company the form creates; the archive goes to format 51 and the record file to 13 (#685)
-
GitHub, Forgejo and SourceHut brand marks appear beside contact links, drawn in black or white where the owner's guidelines allow it, and an identifier scheme can name a mark (#654)
-
Your details can hold a gender, chosen from a short list or in your own words, and a CV prints it only when it is set to (#681)
-
Your details can hold your nationalities, one or several, or only whether you are a citizen of the EU, the EEA or Switzerland, and a CV prints them only when it is set to (#680)
-
Your details has a Personal details card with an optional date and place of birth, which a CV prints as one “Born …” line only when it is set to (#679)
-
Added: links to Mastodon, Bluesky, Facebook, Instagram, YouTube and Codeberg now show the service's own mark beside them where a contact's links are listed, with the generic icon kept for every other service (#654)
-
Added: CONTRIBUTING.md now says to look for an open-source library before writing one and which format an export or import picks first, and pull requests get a template that asks the same questions (#653)
-
The company map is a feature an administrator can switch off under Server settings → Plugins: off, the map page, its button and the guessing of a location's place go, and every stored coordinate stays for when it is back on. (#700)
-
Key caps now sit beside Save and next, Discard and next, Skip to the next and the search box, and Settings → Accessibility has a switch for them apart from the one for the keys; the controls name their key to screen readers either way. (#658)
-
Settings → Appearance lets you choose which identifier schemes are shown and in what order, on a company's page, in Your details, in the companies table's column list and in a CV's choice of rows; hidden ones are counted, never deleted. (#672)
-
Settings → Accessibility can save a field on Appearance and Accessibility as it changes, with the result said beside the heading; off by default, and Save stays. (#656)
-
A listing's description can be written in Markdown (lists, links, emphasis, quotes, headings), chosen per listing, drawn and sanitised on the server; the API and archive carry
description_format. (#665) -
Messaging handles, several per person and per contact, each checked against its service (Matrix, XMPP, Signal, Telegram, Threema or Other): a Messaging block on Your details and on a contact, kept in the archive, the API and a merge, never printed on a CV unless chosen; plugins can add services. (#682)
-
On a phone, tapping the search icon opens the search field in the header in place of the logo and name, with a close button, Escape and the navigation menu's Search all working; with scripts off the icon still opens the search page. (#350)
-
The language picker in Settings can be searched by a language's own name or tag, and says plainly that a partly translated language shows English where its translation stops; TRANSLATING.md describes adding a language without a developer (#72)
-
On a phone, controls grow to 44px where the pointer is coarse, the Applications board shows one column at a time with swipe snapping, and the browser suite checks every page at 390px in both themes (#73)
-
The wiki and the README now show the interface: one command, scripts/screenshots.py, retakes every picture from a seeded demo instance (and --check names stale ones), and the release steps run it. (#353)
-
New issue on Forgejo now offers a Bug report form (version, install, database, browser, steps) and a Feature request form, with a link above them telling reporters to send security problems privately (#351)
-
A CV and a letter can be downloaded as OpenDocument (.odt); each file's title, author, subject, keywords and language are shown before export and can be edited or left out, and what was sent records the choice (#480)
-
Server settings gains a Backups page for administrators: list, back up now, download and delete (with re-authentication), a daily or weekly schedule with retention, backup metrics, and a verified restore guide that prints the commands. (#242)
-
Companies, contacts, listings and applications can each be exported as a JSON file of their own from Settings > Your data and read back through a review page that adds only what is new and never changes what you have. (#659)
-
Leaving a page with unsaved work through a link or a button now asks "Leave without saving?" in Postulo's own translated dialog instead of the browser's prompt, which stays for closing the tab, reload and Back. (#657)
-
Contacts export and import as vCard 4.0 (.vcf): one contact, a company's people, every contact, a company card and your own card out; a 3.0 or 4.0 file in through a review with nothing ticked; the one mapping is on the plugin surface for the DAV plugin. (#660)
-
A company's page now warns, never blocking a save, when its register number and OpenCorporates id name different companies, or a stored identifier would be refused by its own kind today (#675)
-
Events as iCalendar: the calendar downloads as .ics, reminders become VTODO tasks in their own feed, and an .ics file is imported through a review with nothing made until you choose and no time guessed; the writer now runs on the icalendar library (#661)
-
A reference letter now records who wrote it, when, until when it is good to send and who sends it, shows its referee when you record what you sent and on the contact, and is off by default when copying to a new store (#666)
-
An experience's organisation is now linked to one of your companies, added and marked "from your career" if none matches, and kept out of the posting, contact and agency pickers, the map and the companies table until you use it for something (#683)
-
An education entry can state its EQF level, 1 to 8, kept by the Europass import beside the grade, and a CV can print it as "EQF level 7" under a new switch, off by default (#684)
-
Your career record has a Publications section: papers, books, chapters, theses, datasets and software typed like BibTeX entries, printed as one line on a CV, and carried in the archive and your own record file (#687)
-
A spoken language on your career record is chosen from a list instead of typed, and a CV prints its name in the CV's own language (Inglês on a Portuguese CV, Anglais on a French one); archive format 50, candidate file format 12 (#689)
-
The wiki's pre-install pages (Home, Installing Postulo, Getting started) can be built into a static site in every language that has them, with an English fallback and a staleness check, and a Weblate setup for translators (#352)
-
Your career gains Honours and awards and Memberships: a CV prints "title, awarded by" with the year and "role, organisation" with "since 2015" or the years, both link to your companies, and a Europass file's memberships are read (#693)
-
The career gains Courses: a title, a provider linked to your companies, dates, the hours, a summary and a link, printed on every CV as "title, provider · 40 hours · year" (#695)
-
Your career gains References, the people who will vouch for you, kept as contacts and printed on a CV only once they have agreed, with a CV switch for "References are available on request" and Europass referees read in as not yet asked (#696)
-
The career gains one section for presentations, conferences and seminars, with a role and a type that print nothing when not stated; a CV prints one line per event and Europass conferences and speaking history are read in (#694)
🐛 Fixed
- The pairing pages head their record column with the noun, not the verb on the status card's button: the two now have separate entries to translate, so no language shows "Enregistrer" or "Erfassen" there (#1178)
- A Europass PDF whose attached CV is not marked as an embedded file, such as one Google Docs wrote, is read through its file specification instead of being called empty, wherever the PDF lists it and whether or not it is compressed. (#318)
- A reminder nested under an application or an appointment comes back from an account archive with the plugin that placed it, its note and the date it was retired, as a top-level one does, not as one somebody made and still open (#1081)
- The browser tests keep their database in a file, so each request the live server answers has a connection of its own: two requests at once no longer share one in-memory SQLite connection and its transactions, which failed a run at random with
no such savepointand then every test after it. (#1024) - A capture of a page nothing could be read from is accepted when a title was typed for it, and its review says nothing read it; with no title it is still refused, naming the field. (#710)
- Adding a contact through the API needs write access to the company, not only read access, so a company shared read-only can no longer be given contacts. (#328)
- A stored date no longer shows as an empty box in British English and other languages that write the day first (#933)
- CI's translation guard no longer crashes on a force-pushed branch: a previous commit it cannot find makes it read the branch since main (#1011)
- A company's LinkedIn row builds a /company/ address and a person's an /in/ address; Xing likewise. (#932)
- On a phone the star and the bin of a telephone row stay level beside the choice of the number that gets you back in. (#931)
- The seven cards of Your details that drew their heading on the border draw it inside the card. (#919)
- Your details on a phone: the masthead keeps the instance's name and logo and the section menu is drawn, now held by browser tests. (#930)
- The section menu of Your details is held on a phone by a browser test. (#929)
- The calendar's Search everything link carries the words searched for, and a template lint refuses a filter inside translated text (#893)
- The social-profile and repository service lists draw every mark in one colour: Bluesky, Mastodon and YouTube in black or white as their owners allow, and Codeberg, Facebook and Instagram, whose owners allow no black version, by their icon (#905)
- A module's icon is centred in its tile on both plugin pages, and the connection pages draw each plugin with the same tile (#901)
- The calendar's period name is centred on the page in every view, between the step buttons and the views, and its picker opens under it (#900)
- The calendar's Next button reads Next › with its chevron after the word, and both chevrons, like the breadcrumb's, turn round on a right-to-left page (#902)
- The kept-page quota of an account holds when uploads arrive together: the total is counted again once a rendering is written, and one that tipped it over is given back (#776)
- The unattended media sweep refuses a pass that would remove most of the files kept, so a wrong or empty database cannot delete every person's documents (#775)
- A plugin repository's address on Server settings → Plugins wraps instead of pushing the page sideways at 320 pixels. (#894)
- Every page under Documents now has the heading “Documents” and its list's name as a subheading, as Listings, Applications, Companies and the Calendar do. (#846)
- A focused address field in Your details (a service's address with its fixed front) is ringed once, by its group, instead of twice. (#868)
- Resizing a column no longer renders the whole list page in the background or leaves a stray "Columns saved." banner, and holding an arrow key on a handle saves once instead of on every repeat. (#521)
- Exporting an account no longer runs a query per contact, application or sent document, so a large export holds the database write lock for far less time. (#557)
- The Listings Applied and Everything tabs no longer make a query per applied row, so they cost the same however many listings you have applied to. (#558)
- A company's page no longer scrolls sideways on a phone, and a contact's link is no longer cut off under wider text spacing (#654)
- Find logo now says why a site gave nothing, and always tries the favicon (#526)
- Fixed: THIRD-PARTY.md now lists the NACE table and the GeoNames and ESCO data downloaded when an instance is provisioned, and a test finds any licence file the register does not name (#579)
- Removed the duplicate Search row from the phone bottom bar's More menu, and More is no longer drawn when nothing is under it (#339)
- The dashboard widget code no longer declares one field three times, and the linter now catches that mistake (#501)
- The Applications board is a box about the height of the window that scrolls inside the page, so every column's name, open or folded, stays on screen however far down a long column you are; the box is a named region you can scroll from the keyboard, and the page scrolls on past it. (#652)
- The doc comment on ResumeItem.order is written once instead of twice, so later edits cannot leave a stale copy (#614)
- Who an interview is with now reads as one translatable phrase with its own list separator, on the interview list and the application page, instead of a bare "with" and a hard-coded comma (#627)
- The API tokens page now translates the name placeholder and lists each token's scopes as translated sentences, one per item, instead of raw keys like "captures, read". (#432)
- The Tags page counts each tag's applications in one query instead of one per tag, so it no longer gets slower as a person keeps more tags. (#554)
- A status change posted with htmx at a table row now redirects as a plain post does, instead of answering an empty row that no page asked for (#402)
- Putting a reminder off until tomorrow or next week on an application's page now keeps keyboard focus on that reminder's actions button instead of dropping it to the start of the page (#520)
- The scheduler's closing-date and quiet-application announcers now handle each person once per pass instead of once per listing or application, cutting the repeated work on large accounts. (#400)
- The monthly dependency upgrade pull request now gets the full CI run through a bot token, says what the workflow ran, and keeps the pre-commit ruff version in step with the lock (#585)
- Importing a spreadsheet now runs as a watched errand in chunks of 200 rows, so a large sheet no longer times out, rolls back or locks the database for everyone else; imported history is not announced to notifiers. (#555)
- A CV's page, preview, downloads and API read its entries a query per kind instead of one per entry and three per skill group, as does the career overview, so large CVs open and export as fast as small ones. (#559)
- The GeoNames city table is indexed once at provisioning into a lookup database, so saving a company with a location no longer parses 32 MB and holds about 190 MB per worker (#399)
- A captured page with thousands of label-like siblings or deeply nested link blocks no longer holds a worker for minutes: remembered places and the fallback's link-farm measure now read the page in one pass (#588)
- Capturing a page without JSON-LD, or from a board with a recipe, now parses its HTML once instead of seven or eight times, so large pages and the extension's batch capture stop tying up a worker. (#591)
- Setting a profile picture or company logo from a phone photo no longer holds a web worker for seconds: the picture is bounded to 2048 pixels and encoded at most twice (#482)
- Binding a capture to a listing you already have now empties the review data kept on the capture, as saving and discarding already did (#523)
⚠️ Deprecated
- API: a contact's single
phone,phone_countryandlinkedin_urlare deprecated in favour of itsphone_numbersandweb_linkslists; they are still taken and returned, and will be removed in a later release. (#934)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads