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",
    "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 = 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 collectors, 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.
  • hq.modules.release.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 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.

Metadata

Release files for django-hq 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-hq 0.2.0
File Size Uploaded
django_hq-0.2.0.tar.gz 67.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-hq 0.2.0
File Interpreter ABI Platform
django_hq-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 134.8 kB

Release files / django_hq-0.2.0.tar.gz

Download URL django_hq-0.2.0.tar.gz
Size 67.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d6bea3ec2de848c922fb8abf1d247da7bf3c7320245a08cfdb9fca7a101b4d85
BLAKE2b-256 checksum
How to use checksums
9378a2ab28743b17c7ac136fbcf1bc31ba40877929747b6bc823f80d4052c069
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release files / django_hq-0.2.0-py3-none-any.whl

Download URL django_hq-0.2.0-py3-none-any.whl
Size 67.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4821f4a2ee9969953b35da86ff19b89c2d3db714dac2d3fb2ad5d4ffec2eaeb6
BLAKE2b-256 checksum
How to use checksums
7697ce661b332fdaf25dfad85ef7cd7dd6240f899f2626ca284c1950dcb8430d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page