Files
volumen/CHANGELOG.md
T

251 lines
15 KiB
Markdown
Raw Normal View History

2026-09-18 12:03:35 +02:00
# Changelog
All notable changes to **Volumen** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [development]
### Added
-
## [1.0.0] - 2026-09-29
### Added
- **First release** of the platform, the API, the admin and the command line.
**Content**
- Posts are Markdown files with a TOML frontmatter block: `title`, `slug`,
`date`, `lang`, `author`, `tags`, `excerpt`, `cover` with alt text and
caption, `series` with `series_order`, `draft`, `publish_at`, `all_langs`,
`aliases`, `translations`, `fediverse_creator`, `doi`, `orcid`, `refs`, and
any key of your own, preserved in the order you wrote it.
- Mathematics: `$…$` and `$$…$$` in a post body render as MathML Core. The
conversion is server-side and carried by scriptorium, whose symbol tables
cover the standard TeX surface, so equations reach every consumer of the
API as native HTML with no JavaScript and no external service. A display
equation may span lines, the shape the papers in the corpus are written in.
A construct no MathML element can carry is never guessed at: it stays
visible as its verbatim TeX in the equation, marked as an error, exactly
where the author wrote it.
- Diagrams: a fenced `mermaid` code block renders server-side to an inline
SVG, with flowcharts (including the historical `graph` spelling) and
sequence diagrams carried in full: every node shape and edge kind,
subgraphs, classes and styles on one side, participants, all arrow kinds,
notes, activations, the block constructs, coloured rects, autonumbering and
dividers on the other. A diagram type outside the two families stays the
code block the author wrote, and a line that does not parse keeps it too,
so nothing is half-drawn.
- Scholarly identifiers are first-class: a post can carry `doi` and `orcid`
in its frontmatter and the editor, and an account can carry its owner's
ORCID, which pre-fills the field of new posts. A DOI is normalised from a
doi.org URL or doi: prefix to the bare `10.…/…` form; an ORCID is checked
to its ISO 7064 check digit. Validation is syntactic and offline: the
engine never calls doi.org or orcid.org.
- Bibliography is first-class: a post carries a `refs` array of tables in its
frontmatter and marks where the list belongs with a `[[refs]]` line in the
body. The engine renders a numbered reference section, links every inline
`[n]` citation to its entry, and turns each entry's DOI, arXiv id and ORCID
into resolver links; hand-written entries keep their verbatim text with
their identifiers made live. A reference whose DOI belongs to another post
of the same instance links to that post instead of leaving for the
resolver.
- Multi-language posts: one file per language, either in the content root or
in a per-language subdirectory, linked by a `translations` map, with
`all_langs` for a post that belongs to every language.
- Scheduled publishing: `volumen publish-due` for cron or a systemd timer, and
the in-process `[scheduler]`, which publishes the due posts at start-up and
then every interval.
- Post revisions: every save archives the previous version under
`posts/.revisions/<slug>/`, pruned to `revision_limit` versions; deleting a
post moves it into that archive, so it stays undoable.
- Post templates, an import of a `.md` file, and a download of any post with
its frontmatter. A template pre-fills the new-post form: its name, title,
slug, tags and body, plus TOML `key = value` lines for any editor input
(`series`, `doi`, `orcid`, `author`, `lang`, `cover`, `excerpt`, …), stored
in `templates.toml` under `[templates.fields]` and shown on the template's
row, so a scientific volume or a short note starts with its series and
author already in place.
- A media library of uploaded images, and cover images per post. Uploads are
stored under a UUID with the extension their bytes carry, WebP, AVIF and
SVG being the recognised formats, an SVG recognised by its root element;
each tile shows the pixel size read from the container headers or from the
SVG root's size attributes and `viewBox`, and the library takes several
uploads at once, filters by name, and offers copy-link, open and delete on
every tile.
**Public API** under `/api/volumen/`
- Site metadata, paginated post lists with `lang`, `tag` and `q` filters,
single posts with raw Markdown, rendered HTML and a table of contents,
several posts in one request, tag and series listings, RSS 2.0, Atom 1.0,
JSON Feed 1.1 and an XML sitemap.
- The `q` search ranks its results by relevance across the title, the tags,
the excerpt and the body: a title hit leads, and equal scores keep the
date order.
- Pagination in two shapes: page numbers with `has_next` and `has_prev`, or a
cursor with `next_cursor`.
- `ETag` on the list responses and on a post detail, `If-None-Match` answered
with `304`, and `PUT` and `DELETE` honouring `If-Match`, so a write is
refused with `412` instead of silently overwriting a change the client
never saw.
- A post's `doi` and `orcid` appear in the payload and in the JSON-LD block
as resolvable identifiers, and a post with `refs` carries its `references`
array, each entry's DOI, arXiv id and ORCID turned into a resolver link and
the JSON-LD block carrying the `citation` objects beside them: the citation
the web can hand to a reference manager.
- Frontmatter keys the engine does not consume itself pass through in a
`fields` object on a post detail, with nested tables, arrays and date-times
intact.
- A sliding-window rate limit per client address with `X-RateLimit-*` headers
and a `429` carrying `Retry-After`.
- Personal access tokens with the `write` and `delete` scopes for `POST`,
`PUT` and `DELETE`, for publishing from scripts and CI. Only the SHA-256
digest is stored, and the raw token is shown once.
- One error envelope for every failure, with a machine-readable `error` code
and, where it helps, a `message` and a `field`.
**Admin** under `/admin/`
- A first-run wizard founds the installation in place of the login: it
creates the administrator account, takes the interface language and the
colour scheme with a live preview, shows a password strength meter against
the configured policy, and ends with the operator signed in. The first
account is created in one write and under a lock, so two visitors claiming
a fresh installation cannot both open an identity.
- Username and password login with the `admin` and `author` roles, CSRF
tokens on every state-changing form, and a strict Content-Security-Policy
with a per-request nonce. Sessions are bound to the password they were
issued under, so a password change retires every session issued before it,
and an admin can reset another account's password from Settings, Users.
- An optional second factor for any account: time-based one-time passwords
with the enrolment QR drawn by the server itself, one-time recovery codes
shown exactly once and stored only as digests, a replay floor that refuses
a code a second time, and the same lockout guarding code guesses as
password guesses. Nothing changes for an account until its owner finishes
the setup.
- The interface is built on the author's website design system: a layered
stylesheet with `oklch` neutrals and the Viridis, Plasma and Magma palettes
from the exact matplotlib colour-map stops, self-hosted Ubuntu and Ubuntu
Mono, a light/dark/system mode toggle, one shared icon sprite, Graphis, native
dialogs for confirmations and prompts, and a sidebar that folds to an icon
rail remembered on the device. On the post list the status and tag filters
lead with a funnel and a tag glyph and the card's Edit action takes a pencil. The sheets and fonts are served under
`/admin/assets/` and revalidated by a content ETag, so a visit fetches them
once per change. The interface ships in English and Czech, both per-account
choices, and the login screen follows the last one.
- A dashboard with status counts, a tag cloud, search, status filters, bulk
publish, draft and delete, and the next scheduled posts with their publish
dates. The list shows one card per publication: the language versions are
merged by their `translations` frontmatter, the card names the version in
your interface language, and small language chips switch it to another
version; a bulk selection acts on the whole publication, and the counters
and the tag cloud count publications, not files.
- A post editor with a Markdown source view and a visual view over the same
document, live preview, toolbar and keyboard shortcuts, drag-and-drop and
pasted image upload, slug generation, reading-time counters and autosave
with restore. The bibliography has its own card below the editor, as the
reference list sits below the body of a paper: it lists every entry the
post carries, edits the verbatim citation or the structured fields
(authors with ORCID, venue, year, volume, pages, DOI, arXiv, URL),
reorders and removes entries, inserts the `[[refs]]` marker into the body,
names its count in the heading and scrolls inside itself. A save writes
the list back as `[[refs]]` tables with every other frontmatter key
untouched and the identifiers checked on the way in: a DOI normalises from
a doi.org URL, an ORCID checks its own digit. The live preview splices in
the saved post's bibliography, so the author sees what the page will show.
- Revision history per post, with download of any revision, a line-difference
comparison against the current content and one-click restore, and an undo
for the last delete.
- Settings: account (password, username, display name, fediverse handle,
profile photo), users and roles, post templates, the media library, API
tokens, webhooks with a test delivery and per-endpoint enable and remove
that apply without a restart, backup and restore, the version panel with a
checksum-verified in-place self-update, and the audit log switch.
- The surface reaches a phone: below 760 px a navigation sheet behind a
hamburger button carries the sidebar links, the signed-in user and the
logout button, and every control keeps a visible keyboard focus.
**Command line**
- `serve`, `status`, `doctor`, `check-update`, `export`, `import`,
`publish-due`, `validate` and `version`, each with `-h`, and every
operational failure reported with a non-zero exit code. A stray positional
argument is a usage error.
- A manual page, `man/volumen.1`, documents every subcommand and flag, and
the README links it beside the command list.
**Operations**
- Installing is: copy the binary, run `volumen serve`, open `/admin`. With no
`--config` the server reads `/etc/volumen/config.toml` if it exists, then
`~/.config/volumen/config.toml`, and with neither it runs on per-user
defaults whose state lives under `~/.local/share/volumen` (honouring
`XDG_DATA_HOME`), so a plain start works without root and without writing
any file first. The commented configuration template, `config.toml.example`,
is committed at the repository root.
- A fresh installation presents itself as Volumen: the default site title is
`Volumen` and the description `Powered by Volumen.`, in the API, the feeds
and the admin.
- `volumen doctor` and `volumen validate` check an installation and its
content, `volumen status` reports the installation's state, and a
deployment with no accounts yet reports the first-run wizard as the next
step (a warning, not a failure). `GET /healthz` reports readiness for a
supervisor.
- `volumen export` and `volumen import` move a whole deployment, or the admin
Backup panel does, through the same archive: the posts, media and revisions
under the content directory, plus the users, templates and tokens files.
- The session secret is generated by the server: on first start it writes a
64-character key to `secret.key` beside the users file and reuses it
across restarts, so an operator never edits a config to get sessions that
survive. `[admin].session_key` remains as an explicit override.
- Structured JSON logging with `[server].log_format = "json"`, an append-only
audit log, and outgoing webhooks that notify a front end when a post
changes.
- Every request carries a 16-character id, answered in `X-Request-Id` and
attached to each line the handlers write while serving it, and one access
line per request records the method, the path, the status and the duration.
- `[server].trusted_proxies` names the addresses whose `X-Forwarded-For` may
be believed; an empty list never reads the header.
- Read, header, write and idle timeouts on the server, and a shutdown on
`SIGINT` or `SIGTERM` that drains the requests in flight.
- `NOTICE.md` in the repository reproduces the licence of every Go module the
binary is compiled from and the terms of the artwork embedded in it.
### Security
- Post bodies are rendered by scriptorium and then sanitised by bluemonday
against a strict allowlist, so a body is treated as untrusted even though
its author is authenticated; `<script>`, event handlers, inline styles and
unknown URL schemes do not survive, and links gain
`rel="noopener noreferrer"`.
- Passwords are hashed with scrypt at fixed parameters and compared in
constant time; a stored hash below the policy floor or above a 64 MiB
memory bound is rejected rather than trusted.
- Sessions are HMAC-SHA256-signed cookies, `HttpOnly` and `SameSite=Strict`,
`Secure` when TLS terminates in front. Production refuses to start without
a session key of at least 64 bytes.
- The admin refuses a state-changing request a browser sends from another
origin before any handler runs, using the standard library's fetch-metadata
rule: `Sec-Fetch-Site` when the browser sends it, the `Origin` header
against `Host` otherwise. The per-session CSRF token stays as the inner
guard, because it also covers a same-site request from another port.
- Login attempts are limited per address, and the failure message is generic,
so neither the rate limit nor the response time reveals whether an account
exists.
- Uploads are accepted only when the bytes carry a WebP or AVIF signature or
the root element of an SVG, and the stored extension comes from those bytes
rather than from the file name or the declared type.
- Media names, slugs, revision names and backup-archive members are confined
to their directories, and a restore writes through an `os.Root`; an archive
cannot plant a document where the public media route would serve it.
- The self-update verifies the download against the release's `checksums.txt`
and refuses a version that is not newer.
- The backend binds to loopback behind a TLS-terminating proxy, and
`trust_proxy` takes the client address from the last `X-Forwarded-For`
entry, the one the proxy appended.