QDG Knowledge Base Read-only viewer QWebHub
overview

Architecture

Version 1 · Initial architecture page: the module registry pattern, URL mounting, and the template/static namespacing constraint — verified directly against qwebhub/modules.py, urls.py, core/views.py, core/templates/hub/index.html, qwebhub/config.py

Historical version

QWebHub Architecture

QWebHub is a shared Django hub that mounts several otherwise-independent Django projects (QDBAdmin, QProcess, QDG KB Viewer) under one server, each under its own URL prefix, plus a landing page listing them.

The module registry (the core pattern)

Everything routes through one file: qwebhub/modules.py. A Module is a frozen dataclass:

@dataclass(frozen=True)
class Module:
    key: str
    url_prefix: str
    type: str  # "django" | "asgi" | "proxy"
    project_subdir: str = ""
    urlconf: str = ""
    app: str = ""
    db_alias: str = "qdb"
    asgi_factory: str = ""   # future
    proxy_target: str = ""   # future
    enabled: bool = True
    notes: str = ""

MODULES is a plain list of these. Today it has 3 entries, all type="django": qdbadmin (→ AdminManagement/QDBAdmin), qprocess (→ QProcess), qdgkb (→ QDG KB Viewer, the read-only Knowledge Base viewer, db_alias="qdgwiki"). asgi/proxy types are already scaffolded in the dataclass for future non-Django modules but nothing uses them yet.

Adding a new linked project is one Module(...) entry appended to this list — nothing else needs to change. This was confirmed by direct code reading (see ClickUp task 86d40jy36), tracing through every layer below.

How one registry entry becomes a working link

  1. qwebhub/urls.py loops enabled_django_modules() and appends path(m.url_prefix, include(m.urlconf)) for each — the root URLconf is entirely generated from the registry, never hand-edited per module.
  2. core/views.py's index view reads the same enabled_django_modules() list and passes {key, url_prefix, notes} per module to the template. A separate healthz view returns {"status": "ok", "modules": [list of keys]} as JSON — a lightweight liveness/inventory check.
  3. core/templates/hub/index.html loops over that list and renders one card per module (key, notes, link to /{{ url_prefix }}) — nothing about any specific project is hardcoded in the template.

Each Module.project_root resolves project_subdir against PROJECTS_ROOT (from QWEBHUB_PROJECTS_ROOT env var, or auto-detected as the folder containing QWebHub itself) — so the same registry works unmodified on a dev laptop and on the server, as long as sibling project folders exist at the same relative layout.

Constraint that must never be reintroduced: template/static namespacing

Django's template loader is one flat search list across every configured directory. Each module's own top-level templates (e.g. base.html) must live under a subfolder namespaced to that module (templates/core/base.html for QDBAdmin, templates/racing/base.html for QProcess, core/templates/hub/base.html for the hub itself) — never a bare templates/base.html. A shared, un-namespaced file would silently shadow another module's layout with no error, just a broken-looking page.

This was a real regression, fixed 2026-08-01 across all three modules, and is documented as a hard rule in qwebhub/config.py's module_template_dirs(). The same namespacing applies to static assets (module_static_dirs()) — modules namespace assets below static/<module-key>/ for the same collision-safety reason.

Related pages

Updated by Claude on Aug. 13, 2026, 4:12 a.m.