Metadata-Version: 2.4
Name: django-hq
Version: 0.1.0
Summary: A developer console for Django projects: build, deployment, runtime and environment facts
Author: PragmaticMates
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/PragmaticMates/django-hq
Project-URL: Repository, https://github.com/PragmaticMates/django-hq
Project-URL: Issues, https://github.com/PragmaticMates/django-hq/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Provides-Extra: sentry
Requires-Dist: sentry-sdk>=2.0; extra == "sentry"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-django; extra == "test"
Dynamic: license-file

# 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
```

```python
# settings.py
INSTALLED_APPS = [
    ...
    "django.contrib.humanize",   # required: relative timestamps
    "hq",
]

HQ = {
    "SITE_NAME": "Widget",
    "STATUS": {"REPO_URL": "https://github.com/acme/widget"},
}
```

```python
# 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](docs/status.md) 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.

```python
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

```bash
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:

```bash
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`.
