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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Changelog

### v0.4.0.0 (2026-10-13) — latest
### v0.4.0.1 (2026-10-14) — latest
**Patch release: Vietnamese translations lost in 0.4.0.0, daisyUI modals under Unfold, announcement endpoint**
- **Fixed**: translations that 0.4.0.0 dropped from the Vietnamese catalog. Strings translated with `_()` *inside f-strings* are invisible to `makemessages`, so regenerating the catalog for 0.4.0.0 marked them obsolete and left them out of `django.mo`, so 0.4.0.0 showed them in English: user badges (Inactive / Admin / Bot), every "… has been updated by user …" notification, M2M change notes (added / removed) and the dashboard KPI cards ("progress from last", "in"). The `_()` calls are moved out of the f-strings and the translations restored.
- **Fixed**: dashboard KPI cards are whole translatable sentences (`%(object)s in %(period)s`, `%(count)s successful (%(rate)s%%)`); "day" and "successful" were hard-coded English; no change now shows as a green +0 instead of a red -0; the color classes are literal so Tailwind compiles them.
- **Fixed**: notification bodies ("… has been updated by user … with the following changes") were hard-coded English.
- **Fixed**: an open daisyUI `ui-modal` (announcement, feedback resolve, markdown preview) rendered as a narrow white column without overlay: Unfold's unlayered `dialog { … }` rule beats daisyUI's layered `.ui-modal`. `dialog.ui-modal` now restores the full-screen modal.
- **Fixed**: `/announcement/` returned a 500 when the project-provided `ANNOUNCEMENT_CONTENT_HTML_FILE` template is missing or the setting is empty; it now returns empty content and logs a warning.
- **Tests**: no `_()` inside f-strings; every literal msgid in the package's Python code must be in the compiled Vietnamese catalog (catches strings dropped by `makemessages`); dashboard KPI translated; `dialog.ui-modal` override present; announcement endpoint without a template.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.0.1`, `collectstatic`. No migrations. Docker base images `v0.4.0.1` are published by the release workflow.

### v0.4.0.0 (2026-10-13)
**Feature release: daisyUI multi-theme admin, server monitoring dashboard, django-unfold 0.109, security hardening and performance work**
- **Added**: 35 daisyUI themes selectable from the user menu (**Themes**); each theme also re-colors Unfold and drives its dark mode. `WN_ADMIN_THEMES` limits the picker (`[]` hides it); `WN_THEMES="nord,dracula" bash scripts/tailwind.sh` builds a smaller CSS bundle.
- **Added**: `whiteneuron.monitoring` — Grafana-style **Server monitoring** dashboard (CPU, memory, disk, load, network, disk I/O, request throughput, p50/p95/p99 latency, error rate, Apdex, slowest endpoints, DB/cache latency) with time ranges, auto-refresh, host selector and shared crosshair. Collect with `manage.py monitoring_collector` or the Celery task `whiteneuron.monitoring.tasks.collect_metrics`; configure with `WN_MONITORING`. New dependency: `psutil`.
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ 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.0
v0.4.0.1

## Changelog

### Latest: v0.4.0.0 (2026-10-13)
### Latest: v0.4.0.1 (2026-10-14)
**Patch release: Vietnamese translations lost in 0.4.0.0, daisyUI modals under Unfold, announcement endpoint**
- **Fixed**: translations that 0.4.0.0 dropped from the Vietnamese catalog. Strings translated with `_()` *inside f-strings* are invisible to `makemessages`, so regenerating the catalog for 0.4.0.0 marked them obsolete and left them out of `django.mo`, so 0.4.0.0 showed them in English: user badges (Inactive / Admin / Bot), every "… has been updated by user …" notification, M2M change notes (added / removed) and the dashboard KPI cards ("progress from last", "in"). The `_()` calls are moved out of the f-strings and the translations restored.
- **Fixed**: dashboard KPI cards are whole translatable sentences (`%(object)s in %(period)s`, `%(count)s successful (%(rate)s%%)`); "day" and "successful" were hard-coded English; no change now shows as a green +0 instead of a red -0; the color classes are literal so Tailwind compiles them.
- **Fixed**: notification bodies ("… has been updated by user … with the following changes") were hard-coded English.
- **Fixed**: an open daisyUI `ui-modal` (announcement, feedback resolve, markdown preview) rendered as a narrow white column without overlay: Unfold's unlayered `dialog { … }` rule beats daisyUI's layered `.ui-modal`. `dialog.ui-modal` now restores the full-screen modal.
- **Fixed**: `/announcement/` returned a 500 when the project-provided `ANNOUNCEMENT_CONTENT_HTML_FILE` template is missing or the setting is empty; it now returns empty content and logs a warning.
- **Tests**: no `_()` inside f-strings; every literal msgid in the package's Python code must be in the compiled Vietnamese catalog (catches strings dropped by `makemessages`); dashboard KPI translated; `dialog.ui-modal` override present; announcement endpoint without a template.
- **Upgrade**: `pip install -U django-whiteneuron==0.4.0.1`, `collectstatic`. No migrations. Docker base images `v0.4.0.1` are published by the release workflow.

### v0.4.0.0 (2026-10-13)
**Feature release: daisyUI multi-theme admin, server monitoring dashboard, django-unfold 0.109, security hardening and performance work**
- **Added**: 35 daisyUI themes selectable from the user menu (**Themes**); each theme also re-colors Unfold and drives its dark mode. `WN_ADMIN_THEMES` limits the picker (`[]` hides it); `WN_THEMES="nord,dracula" bash scripts/tailwind.sh` builds a smaller CSS bundle.
- **Added**: `whiteneuron.monitoring` — Grafana-style **Server monitoring** dashboard (CPU, memory, disk, load, network, disk I/O, request throughput, p50/p95/p99 latency, error rate, Apdex, slowest endpoints, DB/cache latency) with time ranges, auto-refresh, host selector and shared crosshair. Collect with `manage.py monitoring_collector` or the Celery task `whiteneuron.monitoring.tasks.collect_metrics`; configure with `WN_MONITORING`. New dependency: `psutil`.
Expand Down Expand Up @@ -443,7 +453,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.0 bash docker/push-base.sh # push a release, with provenance and SBOM
GIT_REF=v0.4.0.1 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
91 changes: 91 additions & 0 deletions docs/reports/releases/2026-10-14-v0.4.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Release Notes: v0.4.0.1

**Date**: 2026-10-14
**Type**: Patch release (bug fixes, no migrations)
**Scope**: Vietnamese translations, daisyUI modals under Unfold, announcement endpoint

---

## Summary

v0.4.0.1 fixes three issues reported against v0.4.0.0. Vietnamese translations that the release
dropped are restored, daisyUI modals render correctly again under Unfold's dialog styles, and the
announcement endpoint no longer returns a 500 when its project template is missing. New tests stop
the translation problem from coming back.

---

## Fixed

### Translations dropped in 0.4.0.0
`makemessages` (xgettext) can't read `_()` calls *inside f-strings*. When the catalog was regenerated
for 0.4.0.0, the strings used only that way were marked obsolete (`#~`) and left out of `django.mo`,
so they showed in English:

| String | Where |
|---|---|
| Inactive, Admin, Bot | user grid badges and role icons |
| has been updated by user | title of every update notification |
| added, removed | M2M change notes in notifications |
| progress from last, in | dashboard KPI cards |

- The `_()` calls are moved out of the f-strings (`base/admin.py`, `base/models.py`,
`dashboard/views.py`) and the translations are restored. "Admin" (role badge) is now
"Quản trị", and "Bot" stays "Bot".
- Dashboard KPI cards use whole sentences with named placeholders: `%(object)s in %(period)s`
("Hoạt động trong tuần") and `%(count)s successful (%(rate)s%%)`. The hard-coded English "day" and
"successful" are translated. No change now shows as a green +0 instead of a red -0, and the
color classes are literal so Tailwind compiles them.
- Notification bodies ("… has been updated by user … with the following changes") were hard-coded
English (already in 0.3.x). They are now translated.

### daisyUI modals
Unfold styles every `<dialog>` with an unlayered rule: a centered 28rem white card with padding,
`translate(-50%, -50%)` and `position: relative`. Unlayered CSS beats daisyUI's layered `.ui-modal`
whatever the specificity, so an open `ui-modal` rendered as a narrow white column with no overlay.
This affected the announcement dialog, the feedback "resolve" dialogs and the markdown preview.
A `dialog.ui-modal` rule in `styles.css` restores the full-screen modal with its dimmed overlay.
It is the only unlayered Unfold rule on a bare element; the other daisyUI components the package
uses aren't affected.

### Announcement endpoint
`/announcement/` rendered `ANNOUNCEMENT_CONTENT_HTML_FILE` and returned a 500 if the
project-provided template didn't exist, or returned `None` if the setting was empty. Both cases
now return empty content, and a missing template is logged as a warning.

---

## Tests
- No gettext call inside an f-string anywhere in the package.
- Every literal msgid in the package's Python code is present in the compiled Vietnamese catalog
(or translated by Django/Unfold). This catches strings that `makemessages` drops.
- The dashboard KPI card is fully translated in Vietnamese.
- `styles.css` carries the `dialog.ui-modal` override.
- The announcement endpoint works without a template and with an empty setting.

All of these fail on v0.4.0.0 and pass on v0.4.0.1.

---

## Compatibility / Upgrade

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

- No migrations and no settings changes.
- Translators: new msgids `with the following changes`, `%(object)s in %(period)s` and
`%(count)s successful (%(rate)s%%)`; `in` is no longer used.
- Docker base images `django-whiteneuron-{alpine,debian}:v0.4.0.1` are published by the release
workflow; WN-Sense can set `BASE=v0.4.0.1`.

---

## Validation
- [x] `manage.py test whiteneuron`: all tests pass, under the test project and the package's own settings
- [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] Chromium, Vietnamese UI: dashboard KPI cards, user badges, announcement modal
(full-screen overlay in light and dark, Esc closes, no layout shift when closed)
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.0"
version = "0.4.0.1"
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
33 changes: 33 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@
--color-base-300: var(--ui-color-base-300) !important;
}

/*
Unfold styles every <dialog> (unlayered: centered 28rem white card with padding,
translate(-50%,-50%), position relative). Unlayered CSS beats daisyUI's layered
.ui-modal whatever the specificity, so an open ui-modal rendered as a narrow white
column with no overlay. dialog.ui-modal (0,1,1) beats Unfold's dialog (0,0,1) and
restores daisyUI's full-screen modal; the box inside keeps its own styles.
*/
dialog.ui-modal {
position: fixed;
inset: 0; /* also resets Unfold's top/left: 50% */
translate: none;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border-radius: 0;
box-shadow: none;
background-color: transparent;
}
dialog.ui-modal[open],
dialog.ui-modal:target,
dialog.ui-modal.ui-modal-open {
background-color: oklch(0% 0 0 / 0.4);
}
/* daisyUI draws its own overlay; drop Unfold's blurred ::backdrop */
dialog.ui-modal::backdrop {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}

/* Theme picker option: previews a theme using its own colors */
@layer components {
.wn-theme-option {
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.

21 changes: 13 additions & 8 deletions whiteneuron/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,23 @@ def grid_item_header(self, obj):
</div>
'''

# Labels are looked up outside the f-strings: makemessages can't read _() inside them
active_label, inactive_label = _("Active"), _("Inactive")
staff_label, admin_label = _("Staff"), _("Admin")
superuser_label, bot_label = _("Superuser"), _("Bot")

# Status badges với dark mode support
badges = []
if obj.is_active:
badges.append(f'<span class="ui-badge ui-badge-success ui-badge-xs gap-1"><span class="w-1.5 h-1.5 bg-success dark:bg-success-300 rounded-full animate-pulse"></span>{_("Active")}</span>')
badges.append(f'<span class="ui-badge ui-badge-success ui-badge-xs gap-1"><span class="w-1.5 h-1.5 bg-success dark:bg-success-300 rounded-full animate-pulse"></span>{active_label}</span>')
else:
badges.append(f'<span class="ui-badge ui-badge-error ui-badge-xs gap-1"><span class="w-1.5 h-1.5 bg-error dark:bg-error-300 rounded-full"></span>{_("Inactive")}</span>')
badges.append(f'<span class="ui-badge ui-badge-error ui-badge-xs gap-1"><span class="w-1.5 h-1.5 bg-error dark:bg-error-300 rounded-full"></span>{inactive_label}</span>')

if obj.is_staff:
badges.append(f'<span class="ui-badge ui-badge-info ui-badge-xs">{_("Staff")}</span>')
badges.append(f'<span class="ui-badge ui-badge-info ui-badge-xs">{staff_label}</span>')

if obj.is_superuser:
badges.append(f'<span class="ui-badge ui-badge-warning ui-badge-xs">{_("Admin")}</span>')
badges.append(f'<span class="ui-badge ui-badge-warning ui-badge-xs">{admin_label}</span>')

badges_html = ' '.join(badges)

Expand All @@ -205,11 +210,11 @@ def grid_item_header(self, obj):

role= ""
if obj.is_superuser:
role= f'<span class="material-symbols-outlined text-warning" style="font-size:18px" title="{_("Superuser")}">admin_panel_settings</span>'
role= f'<span class="material-symbols-outlined text-warning" style="font-size:18px" title="{superuser_label}">admin_panel_settings</span>'
elif obj.is_staff:
role= f'<span class="material-symbols-outlined text-info" style="font-size:18px" title="{_("Staff")}">badge</span>'
role= f'<span class="material-symbols-outlined text-info" style="font-size:18px" title="{staff_label}">badge</span>'
elif obj.is_bot:
role= f'<span class="material-symbols-outlined text-secondary" style="font-size:18px" title="{_("Bot")}">smart_toy</span>'
role= f'<span class="material-symbols-outlined text-secondary" style="font-size:18px" title="{bot_label}">smart_toy</span>'

# Compact vertical card layout với dark mode support
string = f"""
Expand All @@ -229,7 +234,7 @@ def grid_item_header(self, obj):
<h5>{display_name}</h5>
<p class="ui-card-title justify-center text-xs">{username_display}</p>
<div class="flex flex-wrap items-center justify-center gap-2 mt-3">
{f'<span class="material-symbols-outlined text-success" style="font-size:18px" title="{_("Active")}">check_circle</span>' if obj.is_active else f'<span class="material-symbols-outlined text-error" style="font-size:18px" title="{_("Inactive")}">cancel</span>'}
{f'<span class="material-symbols-outlined text-success" style="font-size:18px" title="{active_label}">check_circle</span>' if obj.is_active else f'<span class="material-symbols-outlined text-error" style="font-size:18px" title="{inactive_label}">cancel</span>'}
{role}
</div>

Expand Down
27 changes: 16 additions & 11 deletions whiteneuron/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ def save(self, *args, **kwargs):
verbose_name = self._meta.verbose_name
obj_link = self.path()
if action == 'create':
title = f'{_("New")} {verbose_name} "{self}" {_("has been created by user")} "{self.created_by}"'
new_label, created_by_label = _("New"), _("has been created by user")
title = f'{new_label} {verbose_name} "{self}" {created_by_label} "{self.created_by}"'
content_html = format_html(
'<p>{} {} <strong>{} ({})</strong> {} <strong>{}</strong></p>',
_('New'), verbose_name, self, self.pk, _('has been created by user'), self.created_by,
Expand All @@ -538,14 +539,15 @@ def save(self, *args, **kwargs):
content_html += format_html('<p>{} <a href="{}">{}</a>.</p>', _('You can view it at'), obj_link, _('this link'))
changed_data = []
else:
title = f'{_("Update")} {verbose_name} "{self}" ({self.pk}) {_("has been updated by user")} "{self.updated_by}"'
update_label, updated_by_label = _("Update"), _("has been updated by user")
title = f'{update_label} {verbose_name} "{self}" ({self.pk}) {updated_by_label} "{self.updated_by}"'
changed_data = [
{'field_name': name, 'old_value': old_value, 'new_value': new_value}
for name, old_value, new_value in fields_changed
]
content_html = format_html(
'{} <strong>{}</strong> ({}) has been updated by user "{}" with the following changes: <ul>{}</ul>',
verbose_name, self, self.pk, self.updated_by,
'{} <strong>{}</strong> ({}) {} "{}" {}: <ul>{}</ul>',
verbose_name, self, self.pk, updated_by_label, self.updated_by, _('with the following changes'),
format_html_join('', '<li>{}: {} -> {}</li>', fields_changed),
)
Notification.notify_superusers(
Expand Down Expand Up @@ -621,27 +623,30 @@ def _resolve_labels(pks):
added_set = set(pk_list)
old_value = [lbl for lbl in current_labels if not any(f"({pk})" in lbl for pk in added_set)]
new_value = current_labels
desc = f"{_('added')}: {added_labels}"
added_label = _('added')
desc = f"{added_label}: {added_labels}"
elif action == 'post_remove':
removed_labels = _resolve_labels(pk_list)
old_value = current_labels + removed_labels
new_value = current_labels
desc = f"{_('removed')}: {removed_labels}"
removed_label = _('removed')
desc = f"{removed_label}: {removed_labels}"
else: # post_clear
old_value = '(all)'
new_value = []
desc = str(_('cleared'))

changed_data = [{'field_name': field_name, 'old_value': old_value, 'new_value': new_value}]

update_label, updated_by_label = _("Update"), _("has been updated by user")
title = (
f'{_("Update")} {instance._meta.verbose_name} "{instance}" ({instance.pk}) '
f'{_("has been updated by user")} "{user}"'
f'{update_label} {instance._meta.verbose_name} "{instance}" ({instance.pk}) '
f'{updated_by_label} "{user}"'
)
content_html = format_html(
'{} <strong>{}</strong> ({}) has been updated by user "{}" with the following changes: '
'<ul><li>{}: {}</li></ul>',
instance._meta.verbose_name, instance, instance.pk, user, field_verbose, desc,
'{} <strong>{}</strong> ({}) {} "{}" {}: <ul><li>{}: {}</li></ul>',
instance._meta.verbose_name, instance, instance.pk, updated_by_label, user,
_('with the following changes'), field_verbose, desc,
)
try:
Notification.notify_superusers(
Expand Down
Loading
Loading