diff --git a/.env.example b/.env.example index e848730f..c498b37d 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,17 @@ POSTGRES_PORT=5432 # Lifetime (seconds) of a gated-document download link (default 7 days). # TRUST_CENTER_DOWNLOAD_TTL=604800 +# The front-end libraries (Bootstrap, htmx, Leaflet, the interface font...) are +# served from this instance, never from a CDN. A Docker image mirrors them at +# build time; a direct install downloads what is missing on its first launch. +# Set to False for an air-gapped install that puts the files in place itself. +# VENDOR_ASSETS_AUTO_DOWNLOAD=True +# Whether the About modal may ask GitHub whether a newer release exists. It is +# the only outbound call the interface makes, it fires only when someone opens +# that modal, and the answer is cached. Set to False where policy forbids the +# instance calling out. +# UPDATE_CHECK_ENABLED=True + # Optional – auto-create a super-admin on first startup # DJANGO_SUPERUSER_EMAIL=admin@example.com # DJANGO_SUPERUSER_PASSWORD=changeme diff --git a/.gitignore b/.gitignore index e8e531f8..1e9dfc06 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,11 @@ cover/ *.mo *.pot +# Front-end libraries, mirrored from the pins in core/dependencies.py by +# `manage.py vendor_assets` (image build, or first launch of a direct install). +# Never committed: the registry is the source of truth, not a copy of the files. +static/vendor/ + # Django stuff: *.log local_settings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 102414fa..c698d316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to Cairn (formerly Fairway) are documented in this file. +All notable changes to Cairn 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). @@ -9,7 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- The About dialog lists the open source libraries the instance is built on, each linking to its official repository, also exposed at `GET /api/v1/dependencies` and through the `list_dependencies` MCP tool. +- The About dialog lists the open source libraries the instance is built on as `owner / library - version` in two columns, each linking to its official repository, also exposed at `GET /api/v1/dependencies` and through the `list_dependencies` MCP tool. +- Front-end libraries are served from the instance instead of a CDN : `manage.py vendor_assets` mirrors them into `static/vendor/` from the pins in `core/dependencies.py`, at Docker build time or on a direct install's first launch, verifying each download against its Subresource-Integrity digest. +- The About dialog reports whether a newer release is published, from the GitHub releases of the project, also exposed at `GET /api/v1/update-check` and through the `check_for_updates` MCP tool (`UPDATE_CHECK_ENABLED=False` to switch it off). + +### Changed + +- Front-end libraries are upgraded to their current releases : Bootstrap Icons 1.13.1, htmx 2.0.10, Tom Select 2.6.2, DOMPurify 3.4.14, Sortable 1.15.7 and ECharts 5.6.0. ## [0.35.1] - 2026-08-28 diff --git a/Dockerfile b/Dockerfile index dd8f0b9f..d0a4fc65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,12 @@ COPY . . RUN echo "${APP_VERSION}" > /etc/app-version +# Mirror the front-end libraries into static/vendor/ so the running container +# never contacts a CDN. Unlike the two commands below this one must NOT be +# tolerated to fail: an image shipped without them renders an unstyled +# interface, and the build machine is the only place with a mirror to reach. +RUN python manage.py vendor_assets + RUN python manage.py compilemessages 2>/dev/null || true RUN python manage.py collectstatic --noinput 2>/dev/null || true diff --git a/README.md b/README.md index 227aa238..a445629a 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,18 @@ built from [`docs/`](docs/README.md). Django 5.2 LTS, PostgreSQL 16, Django REST Framework, Channels and Redis, Bootstrap 5.3 with HTMX and Apache ECharts. No JavaScript build step. -The full inventory of third-party components lives in -[`core/dependencies.py`](core/dependencies.py), and a running instance states it -in the About dialog, at `GET /api/v1/dependencies` and through the -`list_dependencies` MCP tool. +Every front-end library is served from the instance itself, never from a CDN : +an isolated deployment works, and no third party is told who browses it. They +are mirrored into `static/vendor/` from the pins in +[`core/dependencies.py`](core/dependencies.py) - by the Docker build, or on the +first launch of a direct install - and each download is checked against a +Subresource-Integrity digest. `python manage.py vendor_assets` does it by hand. + +That same file is the full inventory of third-party components, and a running +instance states it in the About dialog, at `GET /api/v1/dependencies` and +through the `list_dependencies` MCP tool. The About dialog also says whether a +newer release is published (`GET /api/v1/update-check`, `check_for_updates`); +set `UPDATE_CHECK_ENABLED=False` to keep the instance from asking. ## Licence @@ -91,8 +99,8 @@ available over a network, and you must offer those users the corresponding source. Distributing a modified version triggers the same obligation. Every source file carries an `SPDX-License-Identifier: AGPL-3.0-or-later` -header. Third-party files under `static/vendor/` keep their own upstream -licences and are not covered by this notice. +header. The third-party files mirrored into `static/vendor/` keep their own +upstream licences and are not covered by this notice. ### Trademarks diff --git a/accounts/api/urls.py b/accounts/api/urls.py index c456a784..bb9fd01f 100644 --- a/accounts/api/urls.py +++ b/accounts/api/urls.py @@ -28,6 +28,7 @@ # Third-party components the instance is built on path("dependencies/", views.DependenciesAPIView.as_view(), name="api-dependencies"), + path("update-check/", views.UpdateCheckAPIView.as_view(), name="api-update-check"), # Resource endpoints path("", include(router.urls)), diff --git a/accounts/api/views.py b/accounts/api/views.py index 28bd6756..a011d1a0 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -418,6 +418,23 @@ def get(self, request): return Response({"status": "success", "count": len(data), "data": data}) +class UpdateCheckAPIView(APIView): + """Whether a newer Cairn release is published, and which one. + + Same answer as the About modal (``core.updates``) : ``state`` is one of + ``outdated``, ``current``, ``unknown`` (unreachable, or a build with no + release number) or ``disabled`` (the check is switched off on this + instance). Lets a fleet operator poll their estate for stale deployments. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + from core.updates import update_status + + return Response({"status": "success", "data": update_status()}) + + # ── Helpers ───────────────────────────────────────────────── def _get_client_ip(request): diff --git a/accounts/templates/accounts/activate.html b/accounts/templates/accounts/activate.html index 70b6c412..9e81c09d 100644 --- a/accounts/templates/accounts/activate.html +++ b/accounts/templates/accounts/activate.html @@ -6,12 +6,8 @@