Files
volumen/CHANGELOG.md
T
petrbalvin 98f7f3c9e3
Test / test (push) Successful in 57s
Release / release (push) Successful in 1m11s
fix: add missing CSP style-src nonce and include static SVGs in wheel
2026-07-27 09:03:44 +02:00

20 KiB
Raw Blame History

Changelog

All notable changes to volumen are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Versions ≤ 0.3.0 were implemented in Ruby. The current codebase is Python (FastAPI). Earlier release notes are kept verbatim as historical record.

[development]

[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.

0.4.0 — 2026-07-26

Added

  • 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 metadatapyproject.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).
  • Teststests/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 PyPIrelease.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.
  • Installerinstall.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.
  • Configurationconfig.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).

0.3.0 — 2026-07-12

Added

Content & authoring

  • Fediverse attribution: a fediverse_creator frontmatter field (e.g. @user@mastodon.social) plus a site-wide [site].fediverse_creator default 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 override the site default.
  • 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 (![alt](url "caption")) are wrapped in a <figure> with a <figcaption> and a small i info icon. Images without a title render exactly as before.

Public API

  • 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".

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

  • 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.

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.

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:, font-src 'self', connect-src 'self', form-action 'self', frame-ancestors 'none'.
  • File uploads are rejected (HTTP 413) when exceeding 10 MB (MAX_UPLOAD_BYTES).
  • File uploads are restricted to image/webp and image/avif (ALLOWED_UPLOAD_TYPES). Anything else is rejected with a 415 and a conversion hint.

0.2.0 — 2026-06-25

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.
  • <language> element in the RSS feed channel.
  • Periodic cleanup of stale login_attempts entries to prevent unbounded memory growth.
  • 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.
  • 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.

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.

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.
  • Derived post excerpts now strip HTML tags before stripping markdown markup, avoiding raw HTML leaks in auto-generated excerpts.
  • 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.
  • A warning is emitted when admin.session_key is configured but shorter than 64 bytes, instead of silently falling back to a random secret.

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.