Added
AI observability module:
djust.observability— DEBUG-gated, localhost-only HTTP endpoints that give external tooling (like the djust Python MCP and djust-browser-mcp) live visibility into framework state without in-process coupling. Ships as seven endpoints under/_djust/observability/:health,view_assigns,last_traceback,log_tail,handler_timings,sql_queries,reset_view_state,eval_handler. Each pairs with a matching MCP tool. Security model mirrors django-debug-toolbar (DEBUG=True +LocalhostOnlyObservabilityMiddleware). Requirespath("_djust/observability/", include("djust.observability.urls"))in the project urls.py.get_view_assigns— Real server-sideself.*state of the mounted LiveView for a given session. Complements browser-mcp's client-onlydjust_state_diffwith the source of truth. Per-attr fallback tags non-serializable values with{_repr, _type}rather than an all-or-nothing blanket.get_last_traceback— Ring-buffered (50) exception log populated fromhandle_exception(). Replaces "can you paste the terminal?" for 80% of blind-debugging cases.tail_server_log— Ring-buffered (500) Django/djust log records withsince_ms+levelfilters.djust.*captured at DEBUG+,django.*at WARNING+. Seedocs/website/guides/mcp-server.md.get_handler_timings— Per-handler rolling 100-sample distribution (min/max/avg/p50/p90/p99). Reuses existingtiming["handler"]measurements; no extra perf counters.get_sql_queries_since— Per-event SQL capture viaconnection.execute_wrappers. Queries are tagged with(session_id, event_id, handler_name)+stack_topfiltered to skip framework frames.reset_view_state— Replayview.mount()on a registered instance. Clears public attrs, re-invokesmount(stashed_request, **stashed_kwargs). Useful between fixture replays.eval_handler— Dry-run a handler against a live view's current state. Returns{before_assigns, after_assigns, delta, result}. v2dry_run=Trueinstalls aDryRunContextthat blocksModel.save/delete,QuerySet.update/delete/bulk_create/bulk_update,send_mail/send_mass_mail,requests.*, andurllib.request.urlopen— first attempt raisesDryRunViolationand the response surfaces{blocked_side_effect}.dry_run_block=Falserecords without blocking. Process-wide lock serializes dry-runs.find_handlers_for_template(template_path)in djust MCP — Cross-references a template file against every view that uses it, returning dj-* handlers wired in the template and the diff against view handler methods. Catches dead bindings at author time (complements djust-browser-mcp's runtimefind_dead_bindings). Seedocs/guides/djust-audit.md.seed_fixtures(fixture_paths)in djust MCP — Subprocess wrapper aroundmanage.py loaddatafor regression-fixture DB setup.See
docs/website/guides/mcp-server.md.
Fixed
hotreload: suppress empty-patch broadcasts on unrelated file changes (#763) — When a Python file changes that doesn't affect the currently-mounted view, re-render produces zero patches. The old code still broadcast ~14 KB (empty patches + full_debugstate dump) to every connected session. Early-return whenhotreload=True AND patches==[]. Non-hot-reload empty patches still sent (loading-state clear ack needed).client.js: guard 38 unguardedconsole.logcalls (#761) — Perdjust/CLAUDE.mdrule, noconsole.logwithoutif (globalThis.djustDebug)guard. Introduced adjLoghelper in00-namespace.jsand replaced bareconsole.log→djLogacross 12 client modules.console.warn/console.erroruntouched (real problems stay visible in prod).- Observability
DryRunContext._uninstalllogs setattr failures (#759) — Silentexcept Exception: passmeant the process could run indefinitely with a wrappedModel.saveif uninstall partially failed — catastrophic for a dev server. Replaced with alogger.warningso the failure is observable.
Changed
djust.observability+ eval_handler v2 — Side-effect blocking now covers QuerySet bulk writes (#758):QuerySet.update/delete/bulk_create/bulk_updateare patched alongsideModel.save/delete, so a handler that doesModel.objects.filter(...).update(...)correctly raisesDryRunViolationinstead of silently committing.- Observability dry_run tests tightened (#760) — Two tests claimed to verify the record-but-allow contract but only checked detection. Now use
unittest.mockto assert the original callable was actually invoked (call_count == 1) alongside the violation-recorded assertion.