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.

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.2.0.tar.gz (67.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.2.0-py3-none-any.whl (67.8 kB view details)

Uploaded Python 3

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

Hashes for django_hq-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d6bea3ec2de848c922fb8abf1d247da7bf3c7320245a08cfdb9fca7a101b4d85
MD5 a83f6dd1d27765ba779037231599450d
BLAKE2b-256 9378a2ab28743b17c7ac136fbcf1bc31ba40877929747b6bc823f80d4052c069

See more details on using hashes here.

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

Hashes for django_hq-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4821f4a2ee9969953b35da86ff19b89c2d3db714dac2d3fb2ad5d4ffec2eaeb6
MD5 52af57c635aee78f6b54398a9dde540b
BLAKE2b-256 7697ce661b332fdaf25dfad85ef7cd7dd6240f899f2626ca284c1950dcb8430d

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