Building a Django Admin-like dashboard with automatic refresh using FastHTML and HTMX
Posted on 2025-02-21 by Simon Moisselin
Django admin is fantastic for quick starts - free UI generated from django models. At our online school, we use it for platform monitoring, course creation, and debugging.
But over time, cracks appeared as our models grew more complex:
For 2,
"The transaction cost of context switching between admin sections became higher than the value they provided." As a result, no-one wanted to use the admin dashboard.
Django admin is a great multi-purpose tool, but we needed specific UIs for our use cases like monitoring and debugging. Rather than using a general solution, let's build a focused dashboard for platform monitoring.
Three core metrics needed atomic monitoring:
Here's the visual solution I built in 142 lines of Python:
The user card implementation looks like a pandas dataframe operation:
def render_user(user: User, attempts_user: List[Attempt]):
# Calculate engagement metrics using native Python collections
stats = {
'success': sum(1 for a in attempts_user if a.is_success),
'failed': len(attempts_user) - sum(1 for a in attempts_user if a.is_success),
'exercises': len({a.exercice.id for a in attempts_user}),
'languages': {a.exercice.language for a in attempts_user}
}
# Time delta calculation without ORM dependencies
duration = max(a.created for a in attempts_user) - min(a.created for a in attempts_user)
return Card(
DivLAligned(
DiceBearAvatar(user.email, h=24, w=24),
Div(cls='space-y-2')(
H3(user.email.split("@")[0], cls='truncate max-w-[300px]'),
DivHStacked(tag_school(user), Tags(stats['languages']), cls='gap-2')
)
),
Div(cls='space-y-2 mb-4')(
P(f"Success: {stats['success']}", cls='text-green-600 font-medium'),
P(f"Failed: {stats['failed']}", cls='text-red-600 font-medium'),
P(f"Exercises: {stats['exercises']}", cls='font-medium')
),
footer=DivFullySpaced(
P(f"{duration.seconds//3600}h{(duration.seconds//60)%60}min", cls='font-bold'),
UkIconLink("mail", height=16, href=f"mailto:{user.email}")
),
cls=CardT.hover + " " + ("bg-white" if tag_school(user) is None else "bg-blue-50")
)
Since LLMs are great at generating html code, this code was quite quick to generate and iterate on. I really like having pydantic / dataclass types as arguments of a frontend rendering function.
HTMX's hx-trigger
enabled 5-minute auto-refresh while avoiding thundering herd problems:
last_refresh = datetime.datetime.now()
@app.get("/api/refresh")
def refresh():
global last_refresh
if needs_refresh(last_refresh): # Global timestamp prevents stampede
fetch_fresh_data() # Single atomic update
last_refresh = datetime.now()
return dashboard()
What is happening here:
This python function returning a paragraph P
will make any page call the /api/refresh
endpoint every 5 minutes.
def rerender_refresh():
return P(
UkIcon("refresh-cw", height=20),
"Auto-refresh every 5 minutes",
cls="text-sm text-gray-600 flex items-center gap-2",
hx_get="/api/refresh",
hx_target="body",
hx_trigger="every 5m"
)
For example, this function will display a grid of users, and will refresh the page every 5 minutes because of the rerender_refresh
component being used.
def render_users(users, attempts):
return Div(
H1(f"{len(users)} utilisateurs, {len(attempts)} tentatives"),
rerender_refresh(), # Adds auto-refresh every 5min via HTMX
Grid(
*(render_user(user, [a for a in attempts if a.user.id == user.id])
for user in users),
gap=4, cols_xl=4, cols_lg=3, cols_md=2, cols_sm=1, cols_xs=1
),
cls='space-y-4'
)
For internal dashboards and dev tools, a simple cookie-based auth is much faster to build than integrating OAuth providers:
beforeware = Beforeware(
user_auth_before,
skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', ]
)
@rt("/login")
def post(password: str, session):
if password == SHARED_PASSWORD:
session['auth'] = 'authenticated' # Simple session storage
return RedirectResponse('/', status_code=303)
return Container(
DivCentered(
Alert("Incorrect password", cls=AlertT.error),
A("Try again", href="/login", cls=AT.primary)
)
)
A Beforeware is just a middleware, that runs before the request from the client is processed by the python server. It checks if the user is authenticated by checking the session cookie inside the user_auth_before
function.
I should probably add filterings, sorting, and searching for each of those type of cards. Also I need more info per cards (number of attempts for the user, etc).
Another quick feature would be to use LLMs to automatically generate a 1-liner description of the activity for each user. Or a summary of the day done by LLMs too.
Also maybe I can have my django objects inside my fasthtml server? or at leasthave a direct db access instead of making api calls to my backend?
Also wanted to better integrate a "mail" button inside the cards to quickly send an email to the user. Ideally sending a mail directly from the dashboard this should be super fast to start interaction with the users based on their activity. But will need to keep track of existing email threads, can be a nightmare.
Now monitoring my platform is easier, no excuses not to do it.
Related to dev tools, it's great to have the freedom from not having to use a general purpose framework and use python. It enabled rapid development - I built a complete dashboard in just one day, with features like color-coded urgency indicators for subscriptions. The simplicity of using plain Python dictionaries for state, HTMX for refreshes, and basic session auth meant the entire codebase stayed under 400 lines across just two files.
I really like Python's built-in functions. It handled data transformations more elegantly than JavaScript's functional methods. Global variables for state management is fine for a short-term solution, that will got you the job done.