Skip to main content

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 = None removes 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 gettext and 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.buildinfo imports 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 moment django.conf.settings is 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

django_hq-0.1.0.tar.gz (61.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_hq-0.1.0-py3-none-any.whl (58.3 kB view details)

Uploaded Python 3

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

Hashes for django_hq-0.1.0.tar.gz
Algorithm Hash digest
SHA256 df16aad8678361683a5f1492346a62f7a0bad460b382ecb0fbe2d0c8cac29240
MD5 ffd047ede74424cc835dee3a0be6d798
BLAKE2b-256 2ce0fae387ad7a180ddfd2ea0cedcabf0a1c8616ba81b9a46b703ea3bd69f36e

See more details on using hashes here.

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

Hashes for django_hq-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 54f16da73dd314ea9708e74565ff7a012c78a9ad51380afdbb3e690bb8ff30be
MD5 1b6596d6a44eded330a6e6a4a49954bd
BLAKE2b-256 ee51bb7bfd58bd08c8bb4e1c648ee400a5bbf91eefea7ce3f015d1dc501429af

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page