• v0.4.2 bce4f5e223

    v0.4.2
    Release / release (push) Successful in 1m11s
    Stable

    petrbalvin released this 2026-07-27 08:38:11 +00:00 | 0 commits to main since this release

    Fixed

    • web/templates/*.jinja missing from distribution wheel — added web/templates/*.jinja to [tool.setuptools.package-data] in pyproject.toml. After a fresh install from PyPI the admin panel returned 500 because Jinja2Templates could not find any templates.
    • Duplicate script-src / style-src in admin CSP — the nonce-augmented directives used to be appended after the base ones, so the browser (which honours the first occurrence) ignored the nonce and blocked every inline JS/CSS. They are now replaced: script-src and style-src are filtered out of the base CSP and the nonce versions are appended as the only occurrence.
    • CSP nonce generated after template renderingrequest.state.csp_nonce is now set in GlobalSecurityHeadersMiddleware.dispatch() before call_next(request) so templates see it during rendering. Previously it was generated afterwards, so csp_nonce was an empty string in the template context and every inline script was blocked.
    • Missing nonce attribute on <script> in list.html.jinja and settings.html.jinja — inline scripts in these templates now carry nonce="{{ csp_nonce | default('') }}" so they match the nonce in the CSP header.
    • Bootstrap admin.password_hash ignored when users.toml is emptyUsers.all now falls back to the bootstrap admin even when users.toml exists but is empty (e.g. right after volumen init, which creates the file via Path.touch()). Previously the first login after a clean install failed.
    • admin_context did not pass users_exist — the login page showed the “No users configured” warning even when authentication worked. Added "users_exist": users_obj.any to the admin template context.
    • Inline style="..." blocked by strict admin CSP — the admin templates used inline style="..." attributes (and one card.style.display = ... JS assignment) which the new strict style-src 'self' 'nonce-…' directive blocks. Replaced every offending attribute with a utility class in the <style> block (page-head-actions, form-options, form-inline, form-hidden, form-spaced, flex-spacer, btn-static, btn-danger-text, text-fg-strong, badge-inline, h3-section, mt-3, is-hidden); the dynamic background-image: url(...) on .post-cover now flows through a --cover-url custom property (style="--cover-url: …") which CSP3 does not treat as an inline style application.
    • /favicon.ico returned 404 — browsers auto-request /favicon.ico even when the page declares an explicit <link rel="icon">. Added a tiny route in create_app() that serves the packaged web/static/volumen-icon.svg with Content-Type: image/svg+xml and a 1-day Cache-Control; added the matching <link rel="icon" type="image/svg+xml" href="/favicon.ico"> to the admin layout so the tab gets the volumen icon instead of a generic placeholder.
    • Sidebar version label rendered as bare vadmin_context did not include the package VERSION, so the footer <div class="sidebar-version">v{{ version }}</div> rendered as just v with no number behind it. Added "version": VERSION to the context.
    Downloads
  • v0.4.1 98f7f3c9e3

    v0.4.1
    Test / test (push) Successful in 57s
    Release / release (push) Successful in 1m11s
    Stable

    petrbalvin released this 2026-07-27 07:03:44 +00:00 | 3 commits to main since this release

    Fixed

    • Missing CSP nonce for style-src on admin routes — added style-src 'self' 'nonce-{nonce}' to the CSP header on /admin routes. Without a nonce the browser blocked every inline <style> tag in the admin templates.
    • web/static/ SVG assets missing from distribution wheel — added a [tool.setuptools.package-data] section to pyproject.toml so volumen-icon.svg and volumen-logo.svg ship inside the installed wheel. The admin panel previously returned 500 on /admin/icon.svg and /admin/logo.svg.
    Downloads
  • v0.4.0 a5c1b519ed

    v0.4.0
    Test / test (push) Successful in 1m0s
    Release / release (push) Successful in 1m10s
    Stable

    petrbalvin released this 2026-07-26 16:19:36 +00:00 | 5 commits to main since this release

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

    Downloads
  • v0.3.0 6f62d3e3f3

    v0.3.0 Stable

    petrbalvin released this 2026-07-12 12:03:22 +00:00 | 12 commits to main since this release

    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.
    Downloads
  • v0.2.0 1440e9a1fd

    v0.2.0 Stable

    petrbalvin released this 2026-06-25 20:14:38 +00:00 | 39 commits to main since this release

    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.
    Downloads
  • v0.1.0 dd3efe870e

    v0.1.0 Stable

    petrbalvin released this 2026-06-21 11:15:06 +00:00 | 62 commits to main since this release

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