v0.2.1: top-bar avatar, You/Admin settings groups, admin models

- Replace the gear/username button with a round initials avatar (per-user
  deterministic color via a new daybook_extras template filter); dropdown
  now shows the username and keeps Settings/About/theme/logout. Sets up an
  optional profile photo later.
- Regroup the settings nav into "You" (Account, API Keys, Storage, Demo
  Data) and a superuser-only "Admin" (Users), About separate — the split
  now reflects who each setting affects, not a flat list.
- Register ApiKey (key read-only) and TagColor in Django admin so /admin/
  is complete as the operator break-glass tool, distinct from the themed
  Settings product surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-04 11:33:23 +01:00
parent 19c1798f90
commit b382eb857b
8 changed files with 134 additions and 12 deletions

View file

@ -8,6 +8,19 @@ the git tags (`vX.Y.Z`) and `daybook_project.__version__`.
_Nothing yet._
## [0.2.1] — 2026-09-04
### Added
- Top-bar **avatar**: a round initials badge (deterministic per-user color)
replaces the gear/username button; lays the groundwork for profile photos.
- Django admin now registers `ApiKey` and `TagColor`, so `/admin/` is complete
as an operator break-glass tool (key value shown read-only).
### Changed
- Settings nav regrouped into **You** (Account, API Keys, Storage, Demo Data)
and a superuser-only **Admin** (Users) section, with About on its own — the
personal/admin split now mirrors who each setting affects.
## [0.2.0] — 2026-09-04
### Added
@ -85,7 +98,8 @@ First tagged release.
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
runbook (`deploy/cloud_agent_deploy.md`) and health check.
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...HEAD
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...HEAD
[0.2.1]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...v0.2.1
[0.2.0]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...v0.2.0
[0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1
[0.1.0]: https://git.cygnus.systems/peter/daybook/releases/tag/v0.1.0

View file

@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Artifact, Asset, UserProfile
from .models import ApiKey, Artifact, Asset, TagColor, UserProfile
@admin.register(Artifact)
@ -21,3 +21,18 @@ class AssetAdmin(admin.ModelAdmin):
list_display = ("id", "owner", "artifact", "content_type", "size_bytes", "created_at")
list_filter = ("owner", "content_type")
search_fields = ("id",)
@admin.register(ApiKey)
class ApiKeyAdmin(admin.ModelAdmin):
list_display = ("name", "user", "key", "created_at", "last_used_at")
list_filter = ("user",)
search_fields = ("name", "key", "user__username")
# The key value and timestamps are system-managed; show but don't hand-edit.
readonly_fields = ("key", "created_at", "last_used_at")
@admin.register(TagColor)
class TagColorAdmin(admin.ModelAdmin):
list_display = ("name", "color")
search_fields = ("name",)

View file

View file

@ -0,0 +1,30 @@
from django import template
register = template.Library()
# A small fixed palette. A user's avatar color is picked deterministically from
# their username, so it stays stable across sessions and page loads.
_AVATAR_COLORS = [
"#4a6ea8",
"#3f8f7b",
"#a3712a",
"#8a4f9e",
"#a3312a",
"#2f7d95",
"#6b7280",
"#7b6a3f",
]
@register.filter
def avatar_color(value):
text = str(value or "")
if not text:
return _AVATAR_COLORS[0]
return _AVATAR_COLORS[sum(ord(c) for c in text) % len(_AVATAR_COLORS)]
@register.filter
def avatar_initial(value):
text = str(value or "").strip()
return text[0].upper() if text else "?"

View file

@ -1,4 +1,4 @@
# Single source of truth for the app version. Bump this, commit, then tag the
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
# GET /api/capabilities ("version").
__version__ = "0.2.0"
__version__ = "0.2.1"

View file

@ -128,6 +128,42 @@ body {
border-color: var(--control-border);
}
.avatar-button {
background: transparent;
border: 0;
border-radius: 50%;
cursor: pointer;
line-height: 0;
padding: 2px;
}
.avatar-button:hover .avatar,
.avatar-button:focus-visible .avatar {
box-shadow: 0 0 0 2px var(--content-bg), 0 0 0 4px var(--control-border);
}
.avatar {
align-items: center;
border-radius: 50%;
color: #fff;
display: inline-flex;
font-size: 0.9rem;
font-weight: 600;
height: 2rem;
justify-content: center;
line-height: 1;
width: 2rem;
}
.user-menu-name {
border-bottom: 1px solid var(--control-border);
color: var(--muted);
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.25rem;
padding: 0.5rem 0.8rem;
}
.user-menu-dropdown {
background: var(--surface);
border: 1px solid var(--control-border);
@ -968,6 +1004,21 @@ kbd {
padding-bottom: 0.7rem !important;
}
.settings-nav-group {
display: flex;
flex-direction: column;
margin-bottom: 0.9rem;
}
.settings-nav-heading {
color: var(--muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
padding: 0.3rem 0.7rem;
text-transform: uppercase;
}
.settings-panel {
max-width: 42rem;
padding: 1.6rem 2rem;

View file

@ -1,3 +1,4 @@
{% load daybook_extras %}
<header class="topbar">
<div class="brandline">
<a class="brand-link" href="{% url 'feed' %}"><img class="brand-logo" src="/static/logo.png" alt=""><h1>AgentFeed</h1></a>
@ -6,12 +7,12 @@
{% if topbar_toolbar %}{% include topbar_toolbar %}{% endif %}
</div>
<div class="topbar-actions">
<button class="icon-button" type="button" data-user-menu-toggle aria-haspopup="true" aria-expanded="false" title="{{ request.user.username }}">
&#9881; {{ request.user.username }}
<button class="avatar-button" type="button" data-user-menu-toggle aria-haspopup="true" aria-expanded="false" title="{{ request.user.username }}" aria-label="{{ request.user.username }} menu">
<span class="avatar" style="background: {{ request.user.username|avatar_color }};">{{ request.user.username|avatar_initial }}</span>
</button>
<div class="user-menu-dropdown" hidden data-user-menu>
<div class="user-menu-name">{{ request.user.username }}</div>
<a href="{% url 'settings-account' %}">Settings</a>
<a href="{% url 'settings-demo-data' %}">Demo Data</a>
<a href="{% url 'settings-about' %}">About</a>
<button type="button" data-theme-toggle>Dark mode</button>
<form method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">Log out</button></form>

View file

@ -24,14 +24,25 @@
<main class="settings-layout">
<nav class="settings-nav">
<a class="settings-nav-back" href="{% url 'feed' %}">&larr; Back to Feed</a>
<div class="settings-nav-group">
<span class="settings-nav-heading">You</span>
<a class="{% if active_section == 'account' %}selected{% endif %}" href="{% url 'settings-account' %}">Account</a>
<a class="{% if active_section == 'api-keys' %}selected{% endif %}" href="{% url 'settings-api-keys' %}">API Keys</a>
<a class="{% if active_section == 'storage' %}selected{% endif %}" href="{% url 'settings-storage' %}">Storage</a>
<a class="{% if active_section == 'demo-data' %}selected{% endif %}" href="{% url 'settings-demo-data' %}">Demo Data</a>
</div>
{% if request.user.is_superuser %}
<div class="settings-nav-group">
<span class="settings-nav-heading">Admin</span>
<a class="{% if active_section == 'users' %}selected{% endif %}" href="{% url 'settings-users' %}">Users</a>
</div>
{% endif %}
<div class="settings-nav-group">
<a class="{% if active_section == 'about' %}selected{% endif %}" href="{% url 'settings-about' %}">About</a>
</div>
</nav>
<section class="settings-panel">
{% if messages %}