2026-06-21 13:15:06 +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 ).
2026-07-26 18:15:29 +02:00
> Versions ≤ 0.3.0 were implemented in Ruby. The current codebase is Python
> (FastAPI). Earlier release notes are kept verbatim as historical record.
2026-06-25 21:37:27 +02:00
## [development]
2026-07-27 09:03:44 +02:00
## [0.4.1] — 2026-07-27
### Fixed
- **CSP nonce chybí pro `style-src` v admin routách** — přidán `style-src 'self' 'nonce-{nonce}'` do CSP hlavičky pro `/admin` routy. Bez nonce prohlížeč blokoval všechny inline `<style>` tagy v admin šablonách.
- **`web/static/` SVG soubory chybí v distribučním kole** — přidána sekce `[tool.setuptools.package-data]` do `pyproject.toml` , aby `volumen-icon.svg` a `volumen-logo.svg` byly součástí instalovaného wheelu. Admin panel dříve vracel 500 na `/admin/icon.svg` a `/admin/logo.svg` .
2026-07-26 18:15:29 +02:00
## [0.4.0] — 2026-07-26
2026-06-26 17:40:48 +02:00
### Added
2026-07-26 18:15:29 +02:00
- **`volumen init` ** — a new `init` subcommand is the *operational*
bootstrap (config, data dirs, admin password, optional systemd unit). Run
as root for a system install (default `--systemd` ); pass `--local` for a
per-user install (`~/.config/volumen/` , `~/.local/share/volumen/` ). The
command renders the production-hardened config from the same commented
TOML template (`host = "::1"` , `env = "production"` , `trust_proxy = true` ,
`cookie_secure = true` ), prompts for an admin password (`getpass` ), hashes
it via the existing scrypt helper, generates a 64-byte hex session key,
creates the system user (`useradd --system` ) and the `/etc/volumen/` /
`/var/lib/volumen/` layout, and (with `--systemd` ) installs a hardened
`/etc/systemd/system/volumen.service` and runs
`systemctl daemon-reload && systemctl enable --now` .
- **PyPI publish metadata** — `pyproject.toml` gains `readme` , `keywords` ,
a complete `classifiers` array (Development Status, Framework :: FastAPI,
supported Python versions, OS, Topic, Typing), and a `[project.urls]`
block (Homepage, Repository, Issues, Changelog, Documentation). PyPI now
renders the README on the project page and links back to sourcedock.dev.
- **`volumen status` ** — read-only inspection of an installation. Prints
human output by default or `--json` for tooling; reports `config_exists` ,
`data_dir_exists` , `users_file_exists` , `password_hash_set` ,
`session_key_ok` , `systemd_unit_installed` , `service_active` ,
`admin_user_present` , and any issues found. Exit code is `1` when any
issue is reported.
- **`volumen check-update` ** — compares the installed version (read via
`importlib.metadata` ) with the latest release on PyPI. Uses only
`urllib.request` (stdlib, 5 s timeout); exit code is `0` up-to-date,
`1` update available (suggests `uv tool upgrade volumen` ), `2` PyPI
unreachable / offline / malformed response. `--json` returns the same
fields plus `checked_at` for cron-friendly alerting.
- **`src/volumen/installer.py` ** — a new module exposing `system_install` ,
`user_install` , `inspect` , plus the private `_mutate_config_text` helper
used to apply line-anchored substitutions to the rendered TOML template
(preserves comment blocks byte-for-byte).
- **Tests** — `tests/test_installer.py` adds 15 tests covering both install
flows, the config-template mutator, the systemd-unit rendering,
idempotency / backup behaviour, the `inspect` round-trip, and the
`--systemd` + `--local` CLI guard.
### Changed
- **First-run setup is now `volumen init` ** — replaces the legacy
`install.sh` . The CLI subcommand renders config, creates data dirs,
prompts for an admin password, generates a session key, and (with
`--systemd` ) installs and enables a hardened systemd unit. No bash
required.
- **Publishing now targets PyPI** — `release.yml` runs
`uv publish --token "$PYPI_TOKEN"` after `python -m build` for `v*`
tags. Wheel + sdist continue to be uploaded to sourcedock.dev Releases as a
secondary mirror. End users get `uv tool install volumen` and
`pipx install volumen` as the one-line install path; no checkout
required.
- **Distribution path is `uv tool install` ** (or `pipx install` ) — the
CLI binary is installed globally; the operational bootstrap is delegated
to `volumen init` . The legacy `uv sync` + project-local venv at
`/opt/volumen/.venv` is gone.
- **`Config.load` round-trips config-file `content_dir` / `users_file` ** —
the rendered template places these two keys after the `[server]` header,
so TOML table folding puts them inside `[server]` . `Config.load` now
hoists them to the root so files produced by `volumen init` (and any
hand-edited config) round-trip through the canonical accessors without
relying on CLI overrides.
- **Migrated from Ruby to Python**: the runtime has been rewritten on top of
FastAPI + Uvicorn, replacing Sinatra with FastAPI, kramdown with
python-markdown + pymdown-extensions, Puma with Uvicorn, Bundler with uv,
minitest with pytest, RuboCop with Ruff, and the TOML parser
(`toml-rb` ) with the standard library `tomllib` (plus `tomli-w` for
round-tripping edits). The public API (`/api/volumen/*` ) and the
server-rendered admin (`/admin/*` ) keep the same shape and HTTP semantics.
- **Installer** — `install.sh` now bootstraps `uv` instead of Bundler, runs
`uv sync --frozen` to provision `/opt/volumen/.venv` , and points the
systemd `ExecStart` at the venv-resident `volumen` console script. The
Python runtime is auto-installed on Fedora/openEuler; other distros get a
heredoc with the equivalent `apt` / `pacman` / `zypper` / `apk` lines.
- **CI** — Forgejo Actions moved to `setup-uv` + Ruff + pytest on a single
Python 3.12 job (was: Ruby 3.3/3.4 matrix with Bundler, RuboCop, and
`bundle exec rake test` ). The release workflow builds wheel + sdist via
`python -m build` and uploads them with the Forgejo Releases API.
- **Migration guard** — new `.forgejo/workflows/migration-guard.yml` greps
the tracked tree for Ruby-only terms (`bundle exec` , `RuboCop` , `Sinatra` ,
`Puma` , `kramdown` , `gem build` , `Volumen::` , `Rack::Session::Cookie` ,
`OpenSSL::KDF` , `lib/volumen/` ) and fails the build if any of them leak
back into production paths.
- **Configuration** — `config.toml` gains six new keys surfaced in
`config.toml.example` : `[server].env` (`"production"` enables strict
startup), `[server].trust_proxy` (honour `X-Forwarded-Proto` from
nginx), `[admin].min_password_length` (default `10` ),
`[admin].max_password_length` (default `1024` , caps scrypt work),
`[admin].max_upload_bytes` (default `10485760` , 10 MB), and
`[admin].cookie_secure` (default `false` in dev, set `true` behind
HTTPS). See `docs/configuration.md` for the full schema.
- **Default bind address** — the installer and `config.toml.example`
default to `host = "::"` (IPv6 with automatic IPv4 fallback) so the
service is reachable on both stacks out of the box. Bind to `"::1"`
when running behind a reverse proxy.
- **Test client now uses `httpx2` ** — dev dependencies replace
`httpx>=0.28` with `httpx2>=2.7` (the actively maintained fork by
Pydantic). Starlette 1.3.x prefers `httpx2` in `TestClient` and emits
`StarletteDeprecationWarning` when only legacy `httpx` is installed;
shipping `httpx2` keeps the test suite warning-free. The legacy
`httpx` package is still pulled in via `fastapi[standard]` for the
`fastapi` CLI and remains API-compatible. The unused
`ignore::DeprecationWarning:starlette.testclient` filter is removed
from `[tool.pytest.ini_options]` (it targeted the wrong warning
class and never matched).
### Removed
- `install.sh` — the bash installer is deleted. Use `uv tool install
volumen` (or ` pipx install volumen`) followed by ` volumen init`.
- ` pkg/` build output and ` volumen.gem` (Ruby gem packaging is gone; the
project now ships as a wheel + sdist built with ` python -m build`).
- ` Gemfile`, ` volumen.gemspec`, ` Rakefile`, ` .rubocop.yml` and the
Sinatra-era ` lib/` tree.
- ` .editorconfig` — Ruff (` pyproject.toml`'s ` [tool.ruff]`) is the single
source of truth for Python formatting; modern editors default to UTF-8,
LF, and a trailing newline. Markdown files in this repo use blank lines
between paragraphs (no ` \n` line-break idiom), so trimming trailing
whitespace is safe.
### Security
- **scrypt password hashing** now uses ` hashlib.scrypt` and constant-time
verification via ` secrets.compare_digest` (was: Ruby's ` OpenSSL::KDF`).
- **Session cookies** are signed by Starlette's ` SessionMiddleware`
(` itsdangerous`) with ` HttpOnly` and ` SameSite=Strict`; ` Secure` is added
by ` SecureCookieMiddleware` when the request was forwarded over HTTPS
(only when ` [server].trust_proxy = true`).
- **CSRF tokens** are generated with ` secrets.token_hex(32)` and verified
with ` secrets.compare_digest` on every state-changing admin POST.
- **Markdown sanitisation policy** — the renderer still passes inline HTML
through (authors are the trusted admin); document the trust model
explicitly in ` docs/security.md` so deployments that ingest untrusted
content know to add a sanitiser.
> Note: refine the Security bullets above once the Python agent confirms the
> final cryptographic controls (e.g. whether ` image/*` upload signatures get
> re-validated server-side and whether the CSRF token is rotated on login).
2026-07-12 14:03:14 +02:00
## [0.3.0] — 2026-07-12
### Added
**Content & authoring**
2026-06-26 17:40:48 +02:00
- **Fediverse attribution:** a ` fediverse_creator` frontmatter field (e.g.
` @user@mastodon .social`) plus a site-wide ` [site].fediverse_creator` default
2026-07-12 14:03:14 +02:00
in ` config.toml`. The value is exposed as ` fediverse_creator` on the site
and post payloads, rendered as ` <dc:creator>` in the RSS feed, as
` authors[].name` in the JSON feed, and can be consumed by a front-end to
emit ` <meta name="fediverse:creator">` on rendered pages. Per-post values
2026-06-26 17:40:48 +02:00
override the site default.
2026-07-12 14:03:14 +02:00
- **Cover caption:** a ` cover_caption` post field rendered next to the cover
image so authors can credit photos or add a short blurb.
- **Titled images as ` <figure>`:** Markdown images with a title
(` `) are wrapped in a ` <figure>` with a ` <figcaption>`
and a small ` i` info icon. Images without a title render exactly as before.
**Public API**
2026-06-26 20:49:48 +02:00
- ` has_next` and ` has_prev` boolean flags in the paginated
` /api/volumen/posts` response so clients can navigate pages without
computing boundaries themselves.
- Distinct ` "draft"` error code in ` /api/volumen/posts/:slug` when the post
exists but is a draft, replacing the generic ` "not_found"`.
2026-07-12 14:03:14 +02:00
**Admin**
- **Modern admin UI:** redesigned layout with a sticky top bar, breadcrumbs
on every page, and a unified design language across login, post list,
post form, and settings.
- **Volumen icon:** a dedicated SVG icon shown in the sidebar and on the
login page (served at ` /admin/icon.svg`).
- **Version in sidebar footer** so admins always see which release is
running.
- **Per-user display name** on the user record and admin header.
- **Per-user fediverse handle** editable from the settings page (independent
of the post-level ` fediverse_creator`).
- **Profile photo upload:** each admin user can upload a WebP/AVIF avatar
that is stored alongside posts in the content directory and shown in the
settings page.
- **Tabbed settings page** separating account, profile, and admin sections.
- **Restricted uploads:** only WebP (` image/webp`) and AVIF (` image/avif`)
are accepted, with a 415 error message that points users to ` cwebp` /
` avifenc` for conversion. Reflects the engine's WebP/AVIF-only posture
end-to-end.
- **Polished native inputs** for ` <input type="date|time|file">` so they
match the rest of the admin's design language.
**Tooling**
2026-06-26 20:49:48 +02:00
- ` just lint` recipe for standalone RuboCop checks and ` just fmt` for
auto-fixing lint issues.
- ` config.toml.example` template for local development; ` config.toml` is
now gitignored.
### Changed
- Memoize ` published_posts` within a single request so repeated calls
(posts list, tags, feeds, sitemap) reuse the cached result.
- ` Store#all ` cache now invalidates on individual file mtime changes, not
just on the content directory mtime. Manual edits and ` git pull` are
detected without a restart.
- ` Cache-Control: public, max-age=60` header on all public API responses
for browser and CDN caching.
- Extract feed and sitemap generation into a dedicated ` FeedHelpers` module,
reducing ` Server` from ~500 to ~420 lines.
2026-07-12 14:03:14 +02:00
### Fixed
- Admin post save/delete now correctly clears the in-memory posts cache, so
the post list reflects edits without a manual reload.
- The post form's date field defaults to today when the underlying post has
no date, preventing the picker from showing an invalid empty value.
2026-06-26 20:49:48 +02:00
### Security
- **Content-Security-Policy** header on all ` /admin/*` responses:
` default-src 'self'`, ` script-src 'self' 'unsafe-inline'`,
` style-src 'self' 'unsafe-inline'`, ` img-src 'self' data:`,
2026-07-12 14:03:14 +02:00
` font-src 'self'`, ` connect-src 'self'`, ` form-action 'self'`,
2026-06-26 20:49:48 +02:00
` frame-ancestors 'none'`.
- File uploads are rejected (HTTP 413) when exceeding **10 MB**
(` MAX_UPLOAD_BYTES`).
2026-07-12 14:03:14 +02:00
- File uploads are restricted to ` image/webp` and ` image/avif`
(` ALLOWED_UPLOAD_TYPES`). Anything else is rejected with a 415 and a
conversion hint.
2026-06-26 17:40:48 +02:00
2026-06-25 22:10:43 +02:00
## [0.2.0] — 2026-06-25
2026-06-25 21:37:27 +02:00
### Added
- ` <pubDate>` in RSS feed items, derived from the post date.
- ` <lastmod>` in sitemap entries for SEO.
- In-memory rate limiting on the login endpoint: 10 attempts per 60 seconds per IP.
2026-06-25 21:41:58 +02:00
- ` <language>` element in the RSS feed channel.
- Periodic cleanup of stale ` login_attempts` entries to prevent unbounded memory growth.
2026-06-25 21:51:32 +02:00
- Slug format validation (` [a-z0-9._-]`) preventing path traversal and XSS in templates.
- Username validation in ` change_username` matching the create-user regex.
- Escape-on-output for slug and username in admin template attributes.
- Memoization of ` Store#all ` with mtime-based invalidation, eliminating N× read+parse
per request.
- Atomic file writes (tmp + rename) for posts and ` users.toml`.
- Post language directory routing: non-default-language posts now land in
` content_dir/<lang>/` instead of overwriting root-level files.
- CLI test coverage (version, help, unknown command, option parsing, config bootstrap).
- Test coverage for RSS feed and sitemap endpoints.
2026-06-25 21:55:33 +02:00
- CORS preflight (` OPTIONS /api/volumen/*`) so browsers can reach the JSON API.
- ` Users#all ` memoization with mtime-based invalidation, matching Store#all.
- Session invalidation: deleted users can no longer use existing session cookies.
- CSRF protection on ` POST /admin/preview`.
- Symlink-safe containment in ` Store#media_file ` via ` File.realpath`.
2026-06-25 21:37:27 +02:00
2026-06-25 22:04:44 +02:00
### Added
- **Scheduled publishing:** a ` publish_at` frontmatter field hides posts from the
public API and feeds until their publication date arrives. The admin form
includes a Publish at date picker.
- **JSON Feed:** ` GET /api/volumen/feed.json` serves a jsonfeed.org v1.1 feed
with ` content_html` and ` date_published` per item.
- **Fulltext search:** ` GET /api/volumen/posts?q=…` filters posts by
case-insensitive match in title and body.
2026-06-25 21:37:27 +02:00
### Changed
- Split ` lib/volumen/server.rb` into ` api_routes.rb` and ` admin_routes.rb` modules,
keeping the server class focused on configuration and shared helpers.
### Fixed
- ` admin.session_ttl` was defined but never read; it now controls
` Rack::Session::Cookie` expiration via ` expire_after`.
2026-06-25 21:41:58 +02:00
- Derived post excerpts now strip HTML tags before stripping markdown markup,
avoiding raw HTML leaks in auto-generated excerpts.
2026-06-25 21:51:32 +02:00
- Draft posts no longer leak through the public detail endpoint (` /api/volumen/posts/:slug`).
- ` Config::DEFAULTS` inner hashes are now frozen and ` deep_merge` builds fresh
copies, preventing mutation from silently poisoning later ` Config.load` calls.
2026-06-25 21:55:33 +02:00
- A warning is emitted when ` admin.session_key` is configured but shorter than
64 bytes, instead of silently falling back to a random secret.
2026-06-25 21:37:27 +02:00
2026-06-21 13:15:06 +02:00
## [0.1.0] — 2026-06-20
First public release: a small, file-based Markdown blog engine in CRuby. Posts
are plain files, the engine exposes them as a JSON API and ships a self-hosted
admin — no database, no external services.
### Added
**Content & storage**
- File-based store: each post is a ` .md` file with a TOML frontmatter block
delimited by ` +++`. No database.
- Post fields: ` title`, ` slug`, ` date`, ` lang`, ` author`, ` tags`, ` draft`,
` excerpt`, ` cover`, ` all_langs`, and ` translations` (a ` lang → slug` map).
- Drafts (` draft = true`) are hidden from the public API.
- Excerpt falls back to the first paragraph (~200 chars) when none is set.
- Estimated reading time (~200 words/min) computed per post.
- Multilingual posts: per-language slugs via ` translations`, plus ` all_langs`
to surface a single post under every language.
**Markdown rendering**
- GFM rendering via kramdown: tables, fenced code blocks, task lists, and
strikethrough.
- Automatic heading IDs for in-page anchors.
- Raw HTML in post bodies is passed through (authors are the trusted admin).
**Public JSON API (` /api/volumen`)**
- Read-only endpoints: ` site`, ` posts`, ` posts/{slug}`, ` tags`, ` feed.xml`
(RSS), and ` sitemap.xml`.
- ` posts` supports pagination (` page`, ` limit`, returning ` total` and
` page_size`) and filtering by ` lang` and ` tag`.
- Permissive CORS so a separate front end can consume the API.
**Admin (` /admin`)**
- Server-rendered admin with a sidebar layout and full post CRUD.
- Dual-mode editor: Markdown source (primary) and a visual WYSIWYG, switchable
per post, with a toggleable live preview persisted across sessions.
- Image upload from the editor toolbar into ` content_dir/media`; any image
format is accepted (WebP and AVIF included) and served with the correct
` Content-Type`.
- Cover image field with upload, URL, clear, and preview.
**Multi-user & roles**
- Multiple admin users stored in ` users.toml`, managed from a settings page.
- Two roles: ` admin` (full access incl. user management) and ` author` (posts
only).
- Change password and username; last-admin and self-deletion safeguards.
**Configuration & CLI**
- TOML configuration, auto-generated with a commented template on first run.
- Configurable ` host`/` port` (defaults to ` 9090`), ` content_dir`,
` users_file`, and ` site` metadata.
- CLI: ` serve`, ` hash-password`, ` version`, ` help`, with ` --config`,
` --content`, ` --host`, and ` --port` overrides.
**Deployment**
- ` install.sh` installs the Ruby toolchain from distro packages on Fedora and
openEuler (and prints guidance on other distros), creates the service user
and directories, and installs a systemd unit.
- Deployment docs for systemd and FreeBSD (` rc.d`), plus an nginx reverse-proxy
setup that runs the admin same-origin, with brute-force rate-limiting and an
optional IP allowlist on the login endpoint.
**Project & tooling**
- Documentation set: architecture, configuration, API reference, deployment,
and security.
- ` just` task set (` install`, ` run`, ` dev`, ` build`, ` test`, ` uninstall`) and
Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).
- ` CONTRIBUTING.md` and a minitest suite; clean RuboCop.
### Security
- Passwords hashed with scrypt and verified in constant time.
- Per-form CSRF tokens on every state-changing admin request.
- Session cookies are ` HttpOnly` and ` SameSite=Strict`, and gain the ` Secure`
attribute automatically on HTTPS requests (detected via ` request.ssl?` /
` X-Forwarded-Proto`), so the cookie never travels over plain HTTP in
production.
- Configurable, stable ` session_key` so sessions survive restarts.
- Generic login error message to avoid username enumeration.
2026-07-26 18:15:29 +02:00
[0.1.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.1.0
[0.2.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.2.0
[0.3.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.3.0
[0.4.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.4.0