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",
"STATUS": {"REPO_URL": "https://github.com/acme/widget"},
}
# urls.py
path("hq/", include("hq.urls")),
That is the whole install. /hq/ is the console, /hq/status/ the status page, and
/hq/status/json/ the same facts for anything that reads rather than looks. Everything is
superuser-only by default.
Most of the page 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.
What it reports
| Deployment | commit, subject, branch, version, commits since tag, build number, 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 |
| Sentry | the release events are filed under, empty exactly when Sentry is not reporting |
| Environment | every variable and every setting, secrets reduced to a fingerprint (opt-in) |
Settings
All optional. HQ is one dict; module settings nest under the module's name.
Console
| Key | Default | Meaning |
|---|---|---|
ACCESS_TEST |
superuser-only | Dotted path to callable(user) -> bool. A bad path raises rather than falling back — a typo here would widen access. |
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. |
MODULES |
["hq.modules.status.module.StatusModule"] |
Which modules the console has, in order. |
HQ["STATUS"]
| 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(). |
ENVIRONMENT |
settings.ENVIRONMENT |
A string, an Enum member or a callable; all three work. |
DISK_PATH |
settings.MEDIA_ROOT |
Which filesystem the storage chart measures. |
MEASURE_DIRECTORY |
True |
Whether to walk DISK_PATH at all. |
DIRECTORY_FILE_BUDGET |
5000 |
Past this the walk gives up and the card says so, rather than holding a worker on a large upload tree. |
SHOW_SETTINGS |
False |
Opt-in. A full settings dump is a considered choice in a private project, not a library default. |
SHOW_ENVIRONMENT |
False |
Opt-in, same reasoning. |
Secrets in those 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.
Adding a module
The console is an umbrella; the status page is the first module in it.
from hq.registry import HQModule
class CronModule(HQModule):
slug = "cron"
label = "Scheduled jobs"
icon = "timer"
def get_urls(self):
from myapp import urls
return urls.urlpatterns
def get_tiles(self, request):
return [{"label": "Next run", "value": "in 12 min", "modifier": "accent"}]
Then list it: HQ = {"MODULES": ["hq.modules.status.module.StatusModule", "myapp.CronModule"]}.
Its views inherit hq.views.HQView to get the console chrome, access gate and never-cache
headers. Navigation appears automatically once there is more than one module.
Design notes
- Nothing is fetched from a network. The fifteen interface icons (lucide, ISC) and five 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 function, 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. Translating the page can never rename a field a script reads. hq.modules.status.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 each module'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.1.0.tar.gz.
File metadata
- Download URL: django_hq-0.1.0.tar.gz
- Upload date:
- Size: 61.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 |
df16aad8678361683a5f1492346a62f7a0bad460b382ecb0fbe2d0c8cac29240
|
|
| MD5 |
ffd047ede74424cc835dee3a0be6d798
|
|
| BLAKE2b-256 |
2ce0fae387ad7a180ddfd2ea0cedcabf0a1c8616ba81b9a46b703ea3bd69f36e
|
File details
Details for the file django_hq-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_hq-0.1.0-py3-none-any.whl
- Upload date:
- Size: 58.3 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 |
54f16da73dd314ea9708e74565ff7a012c78a9ad51380afdbb3e690bb8ff30be
|
|
| MD5 |
1b6596d6a44eded330a6e6a4a49954bd
|
|
| BLAKE2b-256 |
ee51bb7bfd58bd08c8bb4e1c648ee400a5bbf91eefea7ce3f015d1dc501429af
|