django-hq
A developer console for Django projects: which commit is live, when its image was built, when the container came up, what is pending, and how full the disk is.
It answers the question you have at the worst possible moment — what is actually running right now? — and it answers it without a CDN, without a build step and without an external service, because those are the first things to be unavailable when you need to ask.
pip install django-hq
# settings.py
INSTALLED_APPS = [
...
"django.contrib.humanize", # required: relative timestamps
"hq",
]
HQ = {
"SITE_NAME": "Widget",
"RELEASE": {"REPO_URL": "https://github.com/acme/widget"},
}
# urls.py
path("hq/", include("hq.urls")),
That is the whole install. /hq/ is the console and /hq/json/ is the same facts for anything
that reads rather than looks. Superuser-only by default.
Most of it works immediately. The build facts — commit, version, build number, image timestamp —
need the container to have been told them, which is what the /build contract
is for.
One page, made of panels
The console is a single page assembled from modules. Each contributes a panel, a fragment of the
JSON, and optionally a few tiles; HQ["MODULES"] decides which appear and in what order.
| Module | Panel |
|---|---|
release |
commit, subject, branch, version, commits since tag, build link, and a to-scale commit → build → deploy bar |
runtime |
container id, uptime, DEBUG, and the live OS / Python / Django / database versions |
migrations |
the whole pending plan, not a count — a number says something is wrong without saying what |
storage |
disk usage and what the application itself accounts for, as two charts |
environment_variables |
every variable, secrets reduced to a fingerprint — not enabled by default |
django_settings |
every setting, same treatment — not enabled by default |
HQ = {
"MODULES": [
"hq.modules.storage.module.StorageModule", # what you care about most, first
"hq.modules.release.module.ReleaseModule",
"hq.modules.runtime.module.RuntimeModule",
],
}
A module that is not listed is never computed. This is not merely a display filter: leaving
django_settings out means the settings walk never runs, and leaving storage out means nothing
walks your media directory.
Settings
All optional. HQ is one dict; module settings nest under the module's slug in upper case.
Console
| Key | Default | Meaning |
|---|---|---|
MODULES |
release, runtime, migrations, storage | Which panels, in what order. |
ACCESS_TEST |
superuser-only | Dotted path to callable(user) -> bool. A bad path raises rather than falling back — a typo here would widen access. |
TITLE |
"Console" |
What the console calls itself, in the heading and page title. |
SITE_NAME |
None |
Appended to the page title. |
HOME_URL |
None |
URL name or literal path the brand mark links back to. None leaves it inert. |
FAVICON |
None |
Static path. Resolved in Python, so a missing entry under manifest storage is not a 500. |
EXTRA_CSS |
[] |
Static paths appended after the console's own stylesheets. |
FONTS_URL |
Google Fonts | Set None for a system font stack — offline deployments, strict CSP, or not wanting the request. |
ENVIRONMENT |
settings.ENVIRONMENT |
A string, an Enum member or a callable; all three work. Leads the tile strip and marks <body data-environment>. |
HQ["RELEASE"]
| Key | Default | Meaning |
|---|---|---|
REPO_URL |
None |
Without it the repository tile and the commit/build links are simply absent. |
PROVIDER |
detected from the URL host | bitbucket, github or gitlab. |
COMMIT_URL_FORMAT |
from provider | "{repo}/commit/{sha}" — the escape hatch for anything else. |
BUILD_URL_FORMAT |
from provider | "{repo}/actions/runs/{number}". |
RELEASE_PACKAGE |
None |
Package name for sentry_release(). |
HQ["STORAGE"]
| Key | Default | Meaning |
|---|---|---|
DISK_PATH |
settings.MEDIA_ROOT |
Which filesystem the chart measures. |
MEASURE_DIRECTORY |
True |
Whether to walk it at all. |
DIRECTORY_FILE_BUDGET |
5000 |
Past this the walk gives up and the panel says so, rather than holding a worker on a large upload tree. |
Secrets in the two dumps are never shown. They are reduced to eight hex characters of an HMAC
keyed off SECRET_KEY — enough to answer did the container pick up the key I rotated by
comparison, and nothing more.
Writing a module
from hq.registry import HQModule
class CronModule(HQModule):
slug = "cron" # its key in /hq/json/ too
label = "Scheduled jobs"
icon = "timer" # a key in hq.icons.LUCIDE_ICONS
width = "half" # or "full" (the default)
def get_tiles(self, request):
return [{"label": "Next run", "value": "in 12 min", "modifier": "accent"}]
def get_panel(self, request):
return {"template": "myapp/cron_panel.html", "context": {"jobs": jobs()}}
def get_json(self, request):
return {"next_run_seconds": 720}
def is_available(self, request):
return bool(settings.CRONTAB_PATH)
Then list it in HQ["MODULES"]. Everything except slug is optional; a module that renders
nothing and reports nothing is legal, merely useless.
Two things worth knowing:
- A panel is rendered with its own context and nothing else. It cannot come to depend on something a neighbouring module happened to provide, and so cannot break by being reordered. The flip side: a context of the wrong shape does not raise — a missing key in a Django template is silently falsy, and you get an empty panel. Test for something only your panel produces.
- Collectors are memoised per request via
hq.caching.per_request, so answering tiles, panel and JSON in one request costs one read. Decorate yours the same way.
Slugs may not collide with the console's own JSON keys (environment, generated_at); the
registry refuses to start rather than let a module shadow one.
Design notes
- Nothing is fetched from a network. The interface icons (lucide, ISC) and brand marks
(Simple Icons, CC0) are vendored as inline SVG. The only optional external request is the
webfont stylesheet, and
FONTS_URL = Noneremoves it. - A fact that cannot be read becomes
None, never an exception. A dead database drops one row rather than taking down the page you opened because the database looked dead. - The page and the JSON read the same collectors, so they cannot drift apart. The JSON keys
are deliberately not the page's labels: page text goes through
gettextand is free to be translated, while timestamps are ISO-8601 with offsets and durations are seconds. hq.modules.release.buildinfoimports nothing from Django and must stay that way. Projects import it from their settings module to label an admin header or a Sentry release, and at that momentdjango.conf.settingsis still being constructed. There is a test that enforces this in a subprocess.
Development
pip install -e ".[test,sentry]"
pytest
The suite mounts the console at /console/ under the namespace console on purpose — every link
is reversed from the request rather than hardcoded, and a suite that used the documented hq/
would never notice if that stopped being true.
One check is worth running by hand before a release, because a local checkout cannot catch it:
python -m build --wheel && unzip -l dist/*.whl | grep -E "templates|static"
Templates and static live under the hq package — including every panel's, since the
app-directories loader only searches installed apps and modules are not apps. If that ever
changes, a wheel missing them fails only in a built image, as TemplateDoesNotExist.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_hq-0.2.0.tar.gz.
File metadata
- Download URL: django_hq-0.2.0.tar.gz
- Upload date:
- Size: 67.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6bea3ec2de848c922fb8abf1d247da7bf3c7320245a08cfdb9fca7a101b4d85
|
|
| MD5 |
a83f6dd1d27765ba779037231599450d
|
|
| BLAKE2b-256 |
9378a2ab28743b17c7ac136fbcf1bc31ba40877929747b6bc823f80d4052c069
|
File details
Details for the file django_hq-0.2.0-py3-none-any.whl.
File metadata
- Download URL: django_hq-0.2.0-py3-none-any.whl
- Upload date:
- Size: 67.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4821f4a2ee9969953b35da86ff19b89c2d3db714dac2d3fb2ad5d4ffec2eaeb6
|
|
| MD5 |
52af57c635aee78f6b54398a9dde540b
|
|
| BLAKE2b-256 |
7697ce661b332fdaf25dfad85ef7cd7dd6240f899f2626ca284c1950dcb8430d
|