Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

### v0.4.0.2 (2026-10-15) — latest
### v0.4.1.0 (2026-10-16) — latest
**Feature release: admin About page (application / base versions and release dates)**
- **Added**: admin **About** page (`/admin/about/`, sidebar *System → About*, URL name `admin:about`). Every staff user sees the application name, version, release date and short build id (`VERSION`, `RELEASE_DATE`, `BUILD_ID` / `GIT_COMMIT`, cut to 12 characters) and the django-whiteneuron version and release date with a changelog link; superusers also see Python/Django/Unfold versions, database engine, debug mode, time zone, server time, process start and uptime.
- **Added**: `APP_INFO_CALLBACK` setting (`"path.to.func"`, `func(request) -> [(label, value), ...]`) for project rows on the About page; a failing callback is logged and skipped.
- **Added**: `whiteneuron.__version__` and `whiteneuron.__release_date__`; a test keeps them in sync with `pyproject.toml` and this changelog.
- **Security**: the About page never shows host name, OS/kernel, IP addresses, paths, database server version, connection settings or a dependency list (fingerprinting / CVE lookup); runtime details are superuser-only.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.1.0`, `collectstatic`. No migrations. Optional settings: `RELEASE_DATE`, `BUILD_ID`, `APP_INFO_CALLBACK`. Projects that define their own `UNFOLD["SIDEBAR"]` add the link themselves (`reverse_lazy("admin:about")`). Docker base images `v0.4.1.0` are published by the release workflow.

### v0.4.0.2 (2026-10-15)
**Patch release: dependency security updates in the locked set (and the Docker base images)**
- **Security (dependencies)**: locked django 6.0.9, gunicorn 26.2.0, cryptography 50.0.2, hiredis 3.4.2, msgpack 1.2.3, Markdown 3.11.1 and ujson 6.0.0 (all patched releases). The Docker base images install exactly this lock, so projects building `FROM django-whiteneuron-*:v0.4.0.2` no longer need to pin these packages above the base. `django>=6.0.9` is now the minimum.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.0.2`. No code, template or migration changes. Docker base images `v0.4.0.2` are published by the release workflow.
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
![django-whiteneuron](https://raw.githubusercontent.com/White-Neuron/django-whiteneuron/main2.0/docs/images/main.png)

## Current Version
v0.4.0.2
v0.4.1.0

## Changelog

### Latest: v0.4.0.2 (2026-10-15)
### Latest: v0.4.1.0 (2026-10-16)
**Feature release: admin About page (application / base versions and release dates)**
- **Added**: admin **About** page (`/admin/about/`, sidebar *System → About*, URL name `admin:about`). Every staff user sees the application name, version, release date and short build id (`VERSION`, `RELEASE_DATE`, `BUILD_ID` / `GIT_COMMIT`, cut to 12 characters) and the django-whiteneuron version and release date with a changelog link; superusers also see Python/Django/Unfold versions, database engine, debug mode, time zone, server time, process start and uptime.
- **Added**: `APP_INFO_CALLBACK` setting (`"path.to.func"`, `func(request) -> [(label, value), ...]`) for project rows on the About page; a failing callback is logged and skipped.
- **Added**: `whiteneuron.__version__` and `whiteneuron.__release_date__`; a test keeps them in sync with `pyproject.toml` and this changelog.
- **Security**: the About page never shows host name, OS/kernel, IP addresses, paths, database server version, connection settings or a dependency list (fingerprinting / CVE lookup); runtime details are superuser-only.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.1.0`, `collectstatic`. No migrations. Optional settings: `RELEASE_DATE`, `BUILD_ID`, `APP_INFO_CALLBACK`. Projects that define their own `UNFOLD["SIDEBAR"]` add the link themselves (`reverse_lazy("admin:about")`). Docker base images `v0.4.1.0` are published by the release workflow.

### v0.4.0.2 (2026-10-15)
**Patch release: dependency security updates in the locked set (and the Docker base images)**
- **Security (dependencies)**: locked django 6.0.9, gunicorn 26.2.0, cryptography 50.0.2, hiredis 3.4.2, msgpack 1.2.3, Markdown 3.11.1 and ujson 6.0.0 (all patched releases). The Docker base images install exactly this lock, so projects building `FROM django-whiteneuron-*:v0.4.0.2` no longer need to pin these packages above the base. `django>=6.0.9` is now the minimum.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.0.2`. No code, template or migration changes. Docker base images `v0.4.0.2` are published by the release workflow.
Expand Down Expand Up @@ -458,7 +466,7 @@ Release is published.
```bash
bash docker/build-base.sh # HEAD, load into local docker
GIT_REF=v0.3.8.0 bash docker/build-base.sh # a tag / branch / commit
GIT_REF=v0.4.0.2 bash docker/push-base.sh # push a release, with provenance and SBOM
GIT_REF=v0.4.1.0 bash docker/push-base.sh # push a release, with provenance and SBOM
VARIANTS=alpine IMAGE_NAMESPACE=myorg WN_PYTHON_VERSION=3.13 bash docker/build-base.sh
```

Expand Down
8 changes: 8 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,14 @@ Read-only dashboard view that renders sidebar apps from UNFOLD settings. Filters
- `IPBlacklistAdmin.block_ip` action on UserActivity records for quick blocking
- Auto Redis cache sync on save/delete

### About page (`/admin/about/`, URL name `admin:about`)

Sidebar **System → About**, open to every staff user:

- **Application**: `NAME`, `VERSION`, `RELEASE_DATE` (ISO date), `BUILD_ID` (falls back to `GIT_COMMIT`, cut to 12 characters), plus rows returned by `APP_INFO_CALLBACK` (a setting, not an environment variable: `"path.to.func"`, `func(request) -> [(label, value), ...]`). Every staff user sees these rows, so never return secrets, URLs with credentials or internal addresses from the callback.
- **Base platform**: installed `django-whiteneuron` version and release date (`whiteneuron.__version__`, `whiteneuron.__release_date__`), link to the changelog.
- **Runtime** (superusers only): Python, Django and Unfold versions, database engine (no server version), debug mode, time zone, server time, process start and uptime. Host name, OS/kernel, IP addresses, paths, connection settings and the full dependency list are deliberately not shown.

---

## 10. Environment Configuration
Expand Down
131 changes: 131 additions & 0 deletions docs/reports/releases/2026-10-16-v0.4.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Release Notes: v0.4.1.0

**Date**: 2026-10-16
**Type**: Feature release (no migrations, no breaking changes)
**Scope**: Admin "About" page: application and base versions, release dates, runtime summary

---

## Summary

v0.4.1.0 adds an **About** page to the admin. Staff can see which version of the application and
of django-whiteneuron is running and when each was released, without asking a developer or reading
deployment logs. Superusers also get a short runtime summary for support. The page shows only what
these readers need. It never shows host or infrastructure details that would help an attacker
fingerprint the server.

---

## Added

### About page
- URL `/admin/about/` (name `admin:about`), sidebar **System → About** (Vietnamese: *Hệ thống →
Thông tin ứng dụng*). Every active staff user can open it; other users are sent to the login
page.

| Card | Who sees it | Content |
|---|---|---|
| Application version / Base version | all staff | the two versions with their release dates |
| Application | all staff | `NAME`, `VERSION`, `RELEASE_DATE`, `BUILD_ID` (12 characters), rows from `APP_INFO_CALLBACK` |
| Base platform | all staff | `django-whiteneuron`, its version and release date, changelog link |
| Runtime | superusers | Python, Django and Unfold versions, database engine, debug mode, time zone, server time, process start, uptime |

- Dates are formatted in the active language ("Oct. 15, 2026" / "Ngày 15 tháng 10 năm 2026"), and
so is the uptime ("2 days, 3 hours" / "2 ngày, 3 giờ"). Django's own `timesince` has no
Vietnamese translation, so the package formats the uptime itself.
- The process start time is read from the OS through `psutil` (already a dependency since 0.4.0.0).
If `/proc` is restricted, the row is left out and the page still renders.
- Light, dark and every daisyUI theme are supported, and on phones the cards stack in one column.

### Settings

| Setting | Environment variable | Default | Notes |
|---|---|---|---|
| `VERSION` | `VERSION` | `"0.3.2"` | existing setting, also used by announcements |
| `RELEASE_DATE` | `RELEASE_DATE` | `None` | ISO date (`2026-10-15`); other text is shown as is |
| `BUILD_ID` | `BUILD_ID`, falls back to `GIT_COMMIT` | `None` | cut to 12 characters |
| `APP_INFO_CALLBACK` | (none, settings only) | `None` | `"path.to.func"`, `func(request) -> [(label, value), ...]` |

```python
# settings.py
APP_INFO_CALLBACK = "myproject.utils.app_info_rows"

# myproject/utils.py
def app_info_rows(request):
return [("Tenant", "acme"), ("Region", "ap-southeast-1")]
```

The callback rows are shown to **every staff user**. Do not return secrets, URLs that contain
credentials, or internal addresses. If the callback raises, the error is logged as a warning from
`whiteneuron.base.app_info` and its rows are skipped.

### Package version
`whiteneuron.__version__` and `whiteneuron.__release_date__` are now defined. A test checks that they
match `pyproject.toml` and the `CHANGELOG.md` heading, so a release can no longer ship with a stale
version on the About page.

---

## Security

The page was reviewed for information disclosure. These are **never** shown, to any user:

- host name, OS and kernel version, CPU architecture
- IP addresses, file system paths, the Python executable
- database server version, connection settings, the cache backend
- the full dependency list with versions (useful for CVE lookup)

Debug mode and the runtime card are visible to superusers only. The view goes through
`admin_site.admin_view` (staff-only, `never_cache`). All values are auto-escaped, including rows from
`APP_INFO_CALLBACK`. Tests check that staff can't see the runtime card and that the runtime card
contains no host name, kernel version or secret-looking values.

---

## Tests
New `whiteneuron/base/tests_about.py` (9 tests):
- anonymous and non-staff users are redirected
- staff see both versions and the formatted release date, but not the runtime card or debug mode
- superusers get the runtime card, with no host name or kernel version in it
- the build id is cut to 12 characters, and a numeric `BUILD_ID` works
- a failing `APP_INFO_CALLBACK` and empty `VERSION` / `RELEASE_DATE` don't break the page
- callback rows are added to the Application card
- `__version__` / `__release_date__` match `pyproject.toml` and `CHANGELOG.md`
- the uptime is formatted in English and Vietnamese

---

## Compatibility / Upgrade

```bash
pip install -U django-whiteneuron==0.4.1.0
python manage.py collectstatic
```

- No migrations. All new settings are optional.
- If the project defines its own `UNFOLD["SIDEBAR"]["navigation"]`, add the link itself:
`{"title": _("About"), "icon": "info", "link": reverse_lazy("admin:about")}`.
- If the project overrides `BaseAdminSite.get_urls`, keep calling `super().get_urls()` so the
`about/` URL stays registered.
- To fill the page in deployments, set `VERSION`, `RELEASE_DATE` and `BUILD_ID` (or `GIT_COMMIT`)
in the environment, for example from the CI pipeline.
- Translators: new msgids `About`, `Application version`, `Base version`, `Base platform`, `Build`,
`Changelog`, `Debug mode`, `Package`, `Process started`, `Release date`, `Runtime`, `Server time`,
`Time zone`, `Version`, `Visible to superusers only`, `On`, and the plurals `%(count)d day`,
`%(count)d hour`, `%(count)d minute`.
- Docker base images `django-whiteneuron-{alpine,debian}:v0.4.1.0` are published by the release
workflow; WN-Sense can set `BASE=v0.4.1.0`.

### Rollback
Revert to v0.4.0.2. There are no schema changes, and the new settings are ignored by older versions.

---

## Validation
- [x] `manage.py test whiteneuron`: all tests pass
- [x] `check` clean; `makemigrations --check`: no changes
- [x] `msgfmt --check`; `makemessages` leaves no untranslated or fuzzy entries
- [x] Tailwind build reproducible (CI "Compiled CSS is up to date")
- [x] `uv build` produces the wheel and sdist
- [x] Chromium: About page as staff and superuser, Vietnamese and English, light and dark, desktop
and phone width (390px)
5 changes: 5 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ WN_ASYNC_TASKS=False
# Phiên bản hiển thị cạnh tên site trong admin (Unfold SITE_VERSION)
# SITE_VERSION=v1.0.0

# Trang "About" trong admin (/admin/about/): phiên bản ứng dụng, ngày phát hành, build
# VERSION=1.0.0
# RELEASE_DATE=2026-10-15
# BUILD_ID=abc1234

# Server monitoring dashboard
MONITORING_INTERVAL=15
MONITORING_RETENTION_DAYS=7
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "django-whiteneuron"
version = "0.4.0.2"
version = "0.4.1.0"
description = "A modern Django Admin extension with enhanced UI/UX, dashboard, feedback, file management, rate limiting, IP blacklisting, and advanced admin integrations built on django-unfold."
authors = [
{name = "White Neuron", email = "anhnt@whiteneuron.ai"}
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions whiteneuron/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keep in sync with pyproject.toml and CHANGELOG.md (checked by the test suite)
__version__ = "0.4.1.0"
__release_date__ = "2026-10-16"
132 changes: 132 additions & 0 deletions whiteneuron/base/app_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Data for the admin "About" page: application, base package and runtime versions."""
import datetime
import logging
import platform
from importlib import metadata

import psutil
from django.conf import settings
from django.db import connection
from django.utils import formats, timezone
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext

import whiteneuron

logger = logging.getLogger(__name__)

BASE_PACKAGE = "django-whiteneuron"
CHANGELOG_URL = "https://github.com/White-Neuron/django-whiteneuron/releases"
BUILD_ID_LENGTH = 12 # a short commit hash identifies the build; longer values are cut


def package_version(name):
try:
return metadata.version(name)
except metadata.PackageNotFoundError:
return None


def parse_date(value):
"""ISO date/datetime (str) -> date, so templates format it in the active language."""
if isinstance(value, (datetime.date, datetime.datetime)):
return value
if not value:
return None
try:
return datetime.date.fromisoformat(str(value)[:10])
except ValueError:
return str(value) # free text such as "Q4 2026": shown as is


def process_started():
try:
return datetime.datetime.fromtimestamp(psutil.Process().create_time(), tz=datetime.timezone.utc)
except Exception: # restricted /proc: leave it out rather than fail
return None


def format_uptime(started):
"""Days/hours/minutes in the active language (Django's timesince has no Vietnamese)."""
seconds = max(0, int((timezone.now() - started).total_seconds()))
days, seconds = divmod(seconds, 86400)
hours, minutes = divmod(seconds // 60, 60)
parts = []
if days:
parts.append(ngettext("%(count)d day", "%(count)d days", days) % {"count": days})
if hours:
parts.append(ngettext("%(count)d hour", "%(count)d hours", hours) % {"count": hours})
if not days:
parts.append(ngettext("%(count)d minute", "%(count)d minutes", minutes) % {"count": minutes})
return ", ".join(parts)


def display(value):
"""Rows hold raw values; dates are formatted here, in the active language."""
if value is None or value == "":
return "-"
if isinstance(value, datetime.datetime):
return formats.date_format(timezone.localtime(value) if timezone.is_aware(value) else value, "DATETIME_FORMAT")
if isinstance(value, datetime.date):
return formats.date_format(value, "DATE_FORMAT")
return value


def _rows(rows):
return [(label, display(value)) for label, value in rows]


def extra_application_rows(request):
"""Rows added by the project: APP_INFO_CALLBACK = "path.to.func", func(request) -> [(label, value)]."""
path = getattr(settings, "APP_INFO_CALLBACK", None)
if not path:
return []
try:
return [(label, value) for label, value in import_string(path)(request) or []]
except Exception:
logger.warning("APP_INFO_CALLBACK %s failed", path, exc_info=True)
return []


def get_app_info(request):
# the running code's version, so it always matches __release_date__ (editable installs too)
base_version = whiteneuron.__version__
application = [
(_("Name"), _(settings.NAME) if getattr(settings, "NAME", None) else None),
(_("Version"), getattr(settings, "VERSION", None)),
(_("Release date"), parse_date(getattr(settings, "RELEASE_DATE", None))),
(_("Build"), str(getattr(settings, "BUILD_ID", None) or "")[:BUILD_ID_LENGTH]),
*extra_application_rows(request),
]
base = [
(_("Package"), BASE_PACKAGE),
(_("Version"), base_version),
(_("Release date"), parse_date(whiteneuron.__release_date__)),
]
info = {
"app_version": display(getattr(settings, "VERSION", None)),
"app_release_date": display(parse_date(getattr(settings, "RELEASE_DATE", None))),
"base_version": display(base_version),
"base_release_date": display(parse_date(whiteneuron.__release_date__)),
"changelog_url": CHANGELOG_URL,
"application": _rows(application),
"base": _rows(base),
"runtime": None,
}
# Superusers only, and deliberately limited: no host name, OS/kernel, IP, paths, database
# server version, connection settings or full dependency list (fingerprinting / CVE lookup).
if request.user.is_superuser:
started = process_started()
info["runtime"] = _rows([
("Python", platform.python_version()),
("Django", package_version("django")),
("Django Unfold", package_version("django-unfold")),
(_("Database"), connection.display_name),
(_("Debug mode"), _("On") if settings.DEBUG else _("Off")),
(_("Time zone"), settings.TIME_ZONE),
(_("Server time"), timezone.now()),
(_("Process started"), started),
(_("Uptime"), format_uptime(started) if started else None),
])
return info
12 changes: 12 additions & 0 deletions whiteneuron/base/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,12 @@
"link": reverse_lazy("admin:base_uablacklist_changelist"),
"permission": "whiteneuron.base.utils.permission_superuser_callback",
},
{
# application / base versions for all staff, runtime details for superusers
"title": _("About"),
"icon": "info",
"link": reverse_lazy("admin:about"),
},
],
},
{
Expand Down Expand Up @@ -919,6 +925,12 @@
# Announcement (displayed in header, can be used for important notices)
######################################################################
VERSION = environ.get("VERSION", "0.3.2")
# Shown on the admin "About" page (/admin/about/) with VERSION
RELEASE_DATE = environ.get("RELEASE_DATE") # ISO date, e.g. 2026-10-15
BUILD_ID = environ.get("BUILD_ID") or environ.get("GIT_COMMIT")
# Extra rows for the "Application" card, shown to every staff user (never put secrets in them):
# APP_INFO_CALLBACK = "path.to.func", func(request) -> [(label, value), ...]
APP_INFO_CALLBACK = None
ANNOUNCEMENT_CALLBACK = "whiteneuron.base.utils.announcement_callback"
# Set to an absolute Path or a template name to enable announcement content.
# Example: ANNOUNCEMENT_CONTENT_HTML_FILE = BASE_DIR / "templates" / "announcement_content.html"
Expand Down
Loading
Loading