Changelog
All notable changes to djust will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
[0.7.0rc1] - 2026-04-24
Added
- Streaming Markdown
{% djust_markdown %}(v0.7.0) — server-side Markdown renderer built onpulldown-cmark 0.12with three safety guarantees wired in at the crate level: raw HTML in the source is escaped (Options::ENABLE_HTMLis never set; because pulldown-cmark 0.12 still emitsEvent::Html/Event::InlineHtmlwhen that flag is off,sanitise_eventre-routes those events toEvent::Textso the writer escapes them),javascript:/vbscript:/data:URL schemes in link/image destinations are rewritten to#(case-insensitive, leading-whitespace tolerant), and inputs larger than 10 MiB (per-call input cap, not a concurrency limiter) are returned as an escaped<pre class="djust-md-toobig">block without invoking the parser. A provisional-line splitter renders a partially-typed trailing line as escaped text inside<p class="djust-md-provisional">, eliminating mid-token flicker for streaming LLM output. Exposed three ways: the{% djust_markdown expr [kwargs] %}tag (registered via the existing Rust tag-handler registry), the Python helperdjust.render_markdown(src, **opts)returning aSafeString, and the PyO3 functiondjust._rust.render_markdown. Kwargs:provisional,tables,strikethrough,task_lists. Note on deviation from plan:autolinkswas dropped from the public surface — pulldown-cmark 0.12 does not expose aGFM_AUTOLINK/ENABLE_AUTOLINKoptions flag, so plain-text URLs stay as text unless wrapped in explicit[text](url)syntax. Will be reconsidered when the upstream parser is bumped. Covered by 24 Rust tests (crates/djust_templates/src/markdown.rs, including regression cases forvbscript:,data:, mixed-caseJavaScript:, leading-whitespace URLs,<iframe>escaping, image-src neutralisation, and the 10 MiB cap) and 14 Python tests (python/djust/tests/test_markdown.py+tests/unit/test_markdown_tag.py), plus 3 A090 system-check tests — 41 total (24 Rust + 14 Python/tag + 3 A090). Demo at/demos/markdown-stream/; full write-up in docs/website/guides/streaming-markdown.md. - Admin widgets & bulk-action progress (v0.7.0) — two additions to
djust.admin_extclose the most-requested gaps in the alternative reactive admin:DjustModelAdmin.change_form_widgets/change_list_widgetsclass attributes accept any list ofLiveViewsubclasses; each is embedded via{% live_render %}on the matching admin page. Permission filtering honourspermission_requiredon the widget class. See docs/website/guides/admin-widgets.md.@admin_action_with_progress(indjust.admin_ext.progress) turns anyDjustModelAdminaction into a background daemon thread and redirects the user to aBulkActionProgressWidgetpage at<admin>/djust-progress/<job_id>/. The page polls the job every 500 ms, re-renders the progress bar / message / log, and wires a Cancel button that atomically flipsdoneandcancelled. Queryset is eagerly pinned to PKs before the thread starts (no lazy-eval foot-guns). Cancellation is cooperative — clicking Cancel flipsprogress.cancelled = True; the action body must periodically checkif progress.cancelled: returnto actually stop (Python cannot safely interrupt a running thread mid-statement).- Server-side permission enforcement —
@admin_action_with_progress(permissions=[...])stampsallowed_permissionson the wrapped action;ModelListView.run_actionnow callsrequest.user.has_perms(allowed)before dispatching the action and raisesPermissionDeniedif the user lacks any declared perm. Closes the gap wherehas_*_permissionreturns True for any staff user. - Bounded server state:
_JOBSis LRU-capped at_MAX_JOBS = 500(oldest entries evicted on insert once the cap is reached), andJob.message/Job.errorare individually truncated to_MAX_MESSAGE_CHARS = 4096on eachprogress.update(...)call.Job.erroris a generic user-facing string ("Action failed — see server logs for details"); the raw exception text lives only on the server-sideJob._error_rawattribute and is always logged at ERROR level vialogger.exception(logger namedjust.admin_ext.progress). - New setting:
DJUST_ASGI_WORKERS(default1) — declares the number of ASGI workers in the deployment. Gates the A073 system check (fires only whenDJUST_ASGI_WORKERS > 1) so single-worker development stays silent. - Defense-in-depth allowlist:
DJUST_LIVE_RENDER_ALLOWED_MODULES(optional) restricts the dotted-path module prefixes that{% live_render %}will resolve — any widget slot path outside the allowlist raisesTemplateSyntaxErrorat render time. - Two new system checks:
djust.A072(warning) fires if a non-LiveViewclass is registered in a widget slot;djust.A073(info, gated onDJUST_ASGI_WORKERS > 1) fires at startup if any admin site hosts a@admin_action_with_progress-decorated action, noting the v0.7.0 single-worker_JOBSlimitation and pointing at the v0.7.1 channel-layer follow-up. - 25 new tests:
python/djust/tests/test_bulk_progress.py(12) +python/djust/tests/test_admin_widgets_per_page.py(13); +A072/A073 check tests inpython/tests/test_checks.py.
{% dj_activity %}+ActivityMixin(v0.7.0) — React 19.2<Activity>parity: pre-rendered hidden regions of a LiveView that preserve their local DOM state (form inputs, scroll, transient JS) across show/hide cycles. The new block tag{% dj_activity "name" visible=expr eager=expr %}...{% enddj_activity %}emits a wrapper<div>carryingdata-djust-activity,data-djust-visible, and — when not visible — the HTMLhiddenattribute plusaria-hidden="true". The body is rendered unconditionally in every pass so local state isn't lost.ActivityMixin(composed intoLiveViewAFTERStickyChildRegistry, BEFOREView) provides the server-side API:set_activity_visible(name, visible),is_activity_visible(name), declarativeeager_activities: frozensetclass attr, and an internal FIFO deferred-event queue (cap 100, overridable viaactivity_event_queue_cap) drained by the WebSocket consumer after everyhandle_event/handle_infodispatch. Client runtime (python/djust/static/djust/src/49-activity.js) exposeswindow.djust.activityVisible(name)and dispatches a bubblingdjust:activity-shownCustomEvent when a panel flips hidden → visible. The event-dispatch gate in11-event-handler.jsdrops events whose trigger sits inside a hidden non-eager activity client-side (stamping_activityon all other events for server-side deferral). The VDOM patcher in12-vdom-patch.jsskips subtree patches targeting nodes inside a hidden non-eager activity so DOM state is preserved. Two new system checks:A070(Warning — missingnameargument) andA071(Error — duplicate activity name within one template). Seedocs/website/guides/activity.mdfor the full guide +{% if %}/{% live_render %}/ sticky /dj-prefetchcomparison matrix. Demo atexamples/demo_project/djust_demos/views/activity_demo.py.- Intent-Based Prefetch (
dj-prefetch, v0.7.0) — hover- and touch-driven navigation prefetch that complements the existing service-worker-mediated hover prefetch. Links opting in with<a dj-prefetch href="...">are prefetched after a 65 ms hover debounce (cancelled onmouseleavebefore the debounce fires) and immediately ontouchstart— mobile users commit to a tap fast, so no debounce is applied there. Prefetch uses<link rel="prefetch" as="document">injection so the browser manages the cache lifecycle (falls back to low-priorityfetch+AbortControllerwhenrelListdoesn't advertise'prefetch'). Same-origin only;javascript:/data:URLs blocked; dedup'd per URL via a Set thatwindow.djust._prefetch.clear()wipes on SPA navigation. Opt out per-link withdj-prefetch="false". Respectsnavigator.connection.saveData. New client surface:window.djust._intentPrefetchfor test/diagnostic access. Scope: client-side only — no new server endpoint. Contract:dj-prefetchis intended for author-controlled navigation links only; don't put it on links that perform state-changing GETs (see the module header inpython/djust/static/djust/src/22-prefetch.jsfor the full safety contract). Seedocs/website/guides/prefetch.mdfor the guide and the SW-hover-vs-intent comparison table. - Server Functions (
@server_function/djust.call(), v0.7.0) — same-origin browser RPC without VDOM re-render. Decorate a LiveView method with@server_functionand invoke it from JavaScript asawait djust.call('<view_slug>', '<fn>', {params}); the return value is JSON-serialized straight back to the caller. The three primitives now split cleanly by intent:@event_handler— WebSocket, triggers a VDOM re-render (UI interactions: click, submit, input).@event_handler(expose_api=True)— HTTP (ADR-008), triggers a re-render AND exposes the handler to mobile / S2S / AI-agent callers via OpenAPI.@server_function— HTTP, no re-render, no OpenAPI, noapi_response/serialize=hooks. Designed exclusively for in-browser RPC; response envelope is the minimal{"result": <value>}. Session-cookie auth + CSRF are both required unconditionally — no auth-class opt-out. Request body shape is strict: only an empty body,{}, or{"params": {...}}are accepted; any other shape (flat objects, wrapped objects with sibling keys) returns400 invalid_body. This deliberately removes the ambiguity where a caller's own field namedparamswould be silently unwrapped and every sibling key dropped. The dispatcher reuses the ADR-008 pipeline unchanged: parameter coercion viavalidate_handler_params,@permission_requiredgating viacheck_handler_permission, and@rate_limitvia the same LRU-capped_rate_bucketsOrderedDict. Both sync andasync deffunctions are supported via_call_possibly_async. Stacking@event_handlerand@server_functionon the same method raisesTypeErrorat decoration time — a function either re-renders the view or returns an RPC result, never both. New URL:POST /djust/api/call/<view_slug>/ <function_name>/, declared BEFORE the catch-all dispatch pattern so it can't be shadowed. New public surface:djust.decorators.server_function,is_server_function,djust.api.DjustServerFunctionView,dispatch_server_function(inpython/djust/api/dispatch.py),iter_server_functions. New client modulepython/djust/static/djust/src/48-server-functions.js(~40 LOC, ~430 B gzipped delta). Demo:examples/demo_project/djust_demos/adds a product-search view demonstrating both features end-to-end. Seedocs/website/guides/server-functions.mdfor the full API reference, error-code table, and comparison vs.@event_handlerand@event_handler(expose_api=True).
[0.6.1rc1] - 2026-04-24
Added
- Time-Travel Debugging (v0.6.1) — dev-only debug-panel tab that
records a state snapshot around every
@event_handlerdispatch (state_before/state_after), then lets developers scrub back through the timeline and jump to any past state. The server restores the snapshot viasafe_setattrand re-renders through the normal VDOM patch pipeline. Opt-in per view (time_travel_enabled = Trueon theLiveViewsubclass); zero cost when disabled. Gated onDEBUG=Trueat the WebSocket consumer so production clients can't coerce a jump even if the class attr is left on. Per-view bounded ring buffer (default 100 events, configurable viaLIVEVIEW_CONFIG["time_travel_max_events"]). New moduledjust.time_travel(EventSnapshot,TimeTravelBuffer,record_event_start,record_event_end,restore_snapshot). New inbound WS frametime_travel_jump+ outboundtime_travel_stateack, plustime_travel_eventframes pushed after every recorded snapshot so the debug panel timeline populates incrementally (client CustomEventdjust:time-travel-event). Instrumentation wraps all three dispatch branches (actor, component, view handler) and records permission-denied / validation-failed events with anerrormarker. Component events record against the parent view in Phase 1 (full component-level time travel is a v0.6.2 follow-up). Ghost-attr cleanup inrestore_snapshotremoves public attributes not present in the target snapshot, so restoring{a:1}over{a:5, b:10}leaves{a:1}rather than{a:1, b:10}. New client eventsdjust:time-travel-stateanddjust:time-travel-event(CustomEvents). New system checksdjust.C501(info — global switch on) anddjust.C502(error — non-positivetime_travel_max_events). Beyond Redux DevTools: server-side so no client state store; beyond Phoenix LiveView's debug tools which are telemetry-only. Seedocs/website/guides/time-travel-debugging.md. - Streaming Initial Render (v0.6.1, Phase 1) — opt-in chunked HTTP
response for LiveView GET requests. Setting
streaming_render = Trueon a LiveView class returns aStreamingHttpResponsethat flushes the page in three chunks: shell-open (everything before<div dj-root>), main content (the<div dj-root>...</div>body), and shell-close (</body></html>+ trailing markup). Phase 1 is transport-layer only — the server fully assembles the rendered HTML before streaming it; the benefit is HTTP/1.1 chunked transfer (noContent-Length, earlier TCP flush, compatibility with chunk-relaying proxies, avoiding gzip-buffer stalls). True server-side render overlap (browser parses shell while server computes main content) arrives with Phase 2 (v0.6.2) alongside lazy-child streaming via{% live_render lazy=True %}. No client-side code changes; opt-in per view, backward-compatible default. Response emitsX-Djust-Streaming: 1for observability and omitsContent-Length. Seedocs/website/guides/streaming-render.md. - Hot View Replacement (HVR, v0.6.1) — state-preserving Python code
reload in development. When a LiveView module changes on disk, the dev
server
importlib.reload()s the module and swaps__class__in place on every live instance of the changed class, then re-renders via the existing VDOM diff path. Users keep form input, counter values, active tab, and scroll position — React Fast Refresh parity for djust. Gated onDEBUG=True+LIVEVIEW_CONFIG["hvr_enabled"](default True). Falls back to full reload on a conservative state-compat heuristic (removed handlers, changed handler signatures, or slot layout drift). New system checkdjust.C401warns when HVR is enabled butwatchdogis not installed. New client eventdjust:hvr-applied(CustomEvent). Zero cost in production.
[0.6.0rc1] - 2026-04-23
Documentation
- CSS
@starting-styleguide section (v0.6.0) — documents that browser-native@starting-styleworks unmodified with djust's VDOM insert path. No new djust attributes or JS — the feature is pure CSS. Guide section indocs/website/guides/declarative-ux-attrs.mdincludes a quick-start example, a side-by-side comparison vsdj-transition(browser support, runtime cost, per-element customization), interop notes withdj-removefor enter+exit coverage, and caveats around@supportsgating for older browsers. ROADMAP parity-tracker row updated to ✅ Documented v0.6.0.
Changed
- Package consolidation sunset — ADR-007 Phase 4 closure (v0.6.0) — the
three-phase consolidation that started in v0.5.0 is now complete. The five
sibling repos (
djust-auth,djust-tenants,djust-theming,djust-components,djust-admin) are sunset atv99.0.0— each retains a shim-only__init__.pythat re-exports fromdjust.<name>and emits aDeprecationWarning. Path A was chosen over PyPI publish: existing releases remain installable indefinitely for legacy projects; no new PyPI versions will ship. djust core now exposes the consolidation via[project.optional-dependencies]—djust[auth],djust[tenants](withdjust[tenants-redis]anddjust[tenants-postgres]backend-specific sub-extras),djust[theming],djust[components],djust[admin]. Two new extras (auth,tenants) added in this release; the others shipped in v0.5.0. ADR-007 status updated from "Proposed" → "Accepted + Phase 4 complete". New migration guide:docs/website/guides/migration-from-standalone-packages.md(mechanical sed script + FAQ + edge cases). Cosmetic tech-debt: sibling repos retain dead pre-consolidation source files next to the shim — cleanup tracked separately; no user impact.
Added
-
Request-path profiling harness (v0.6.0, investigative, ROADMAP Group 5 P2) — reproducible profile of the mount → event → VDOM diff → patch path. New
scripts/profile-request-path.py(cProfile wrapper, optional py-spy hint, writesartifacts/profile-<timestamp>.{txt,pstats}; exits non-zero on target-miss for CI). Newtests/benchmarks/test_request_path.pywith eight pytest-benchmark cases across four groups (HTTP render, WebSocket mount, event dispatch, VDOM diff+patch) with hard assertions against the 2 ms per-event / 5 ms list-update budgets. Newdocs/performance/v0.6.0-profile.mdreporting all measured timings (mount 0.07 ms, event 4 µs, VDOM diff 4 µs, list reorder 0.38 ms — all within targets by at least 5x). Newmake profiletarget wired to the harness (the priormake profileruntime-stats target is nowmake profile-stats). No optimizations were required; the profile confirms the existing Rust-side architecture is well under target. -
Service Worker advanced features (v0.6.0) — three SW-backed optimizations landed in one PR:
- VDOM patch cache: per-URL HTML snapshots served instantly on popstate,
then reconciled against the live WebSocket mount reply. Configurable via
DJUST_VDOM_CACHE_ENABLED/DJUST_VDOM_CACHE_TTL_SECONDS/DJUST_VDOM_CACHE_MAX_ENTRIES. New system checksdjust.C301/C302/C303guard config ranges. - LiveView state snapshots: opt-in per view via
enable_state_snapshot = Trueon aLiveViewsubclass. Client captures JSON-serializable public state ondjust:before-navigate; server restores via_restore_snapshot(state)in lieu ofmount()when the user hits back. Views override_should_restore_snapshot(request)to reject stale snapshots. System checkdjust.C304warns when a snapshot-opt-in view declares attributes matching PII naming patterns. - Mount batching: when multiple
dj-lazyLiveViews hydrate together, the client sends onemount_batchWebSocket frame instead of N separatemountframes. Server responds with onemount_batchcarrying all rendered views; per-view failures are isolated in afailed[]array (atomicity relaxed so one bad view doesn't kill the batch). Opt out viawindow.DJUST_USE_MOUNT_BATCH = false. - New client module
46-state-snapshot.js(~120 LOC); new senders ondjust._sw.cacheVdom/lookupVdom/captureState/lookupState. registerServiceWorker({vdomCache: true, stateSnapshot: true})gates the new behaviors alongside existinginstantShell/reconnectionBridgeoptions.
- VDOM patch cache: per-URL HTML snapshots served instantly on popstate,
then reconciled against the live WebSocket mount reply. Configurable via
Changed
LiveViewConsumer.handle_mount()accepts newstate_snapshotkwarg; dispatches to the snapshot-restore path when the view opts in and the payload'sview_slugmatches. New methodhandle_mount_batch()+_mount_one()collector seam enable the mount-batch path without regressing the single-viewmountflow.
Security
-
State snapshots are JSON-only (no pickle).
safe_setattrblocks dunder keys and private (_-prefixed) attributes during restoration. SW enforces a 256 KB upper bound onstate_jsonpayloads; client clamps at 64 KB. System checkdjust.C304warns when snapshot-opt-in views declare attribute names matchingpassword|token|secret|api_key|pii. -
Sticky LiveViews (v0.6.0) — Phoenix
live_render sticky: trueparity. Shipped across three PRs: #966 (Phase A — embedding primitive), #967 (Phase B — preservation acrosslive_redirect), #969 (Phase C — ADR-011, user guide, demo app). Mark a LiveView class withsticky = True+sticky_idand embed it via{% live_render "myapp.views.AudioPlayerView" sticky=True %}. Destination layouts declare<div dj-sticky-slot="<id>"></div>at the re-attachment point; the same Python instance, DOM subtree, form values, scroll/focus, and background tasks all survivelive_redirectnavigation. Use case: app-shell widgets (audio players, sidebars, notification centers), wizard preview panes.User-facing API
LiveView.sticky: bool = False+sticky_id: Optional[str] = Noneclass attrs.{% live_render "dotted.path" sticky=True %}template tag (validates class opt-in at render time;TemplateSyntaxErroron mismatch).[dj-sticky-slot="<id>"]slot markers in destination layouts.djust:sticky-preserved/djust:sticky-unmountedCustomEvents for lifecycle hooks (reasons:server-unmount,no-slot,auth)._on_sticky_unmount()per-instance hook (default: cancels pendingstart_asynctasks).
Wire protocol
child_update(Phase A) — scoped VDOM patches for embedded non-sticky children.sticky_hold(server→client, sent BEFOREmountonlive_redirect) — enumerates surviving sticky_ids so the client reconciles its stash against the authoritative list. Ordering is load-bearing: the mount handler eagerly reattaches, so a latesticky_holdwould reattach auth-revoked views.sticky_update(server→client) — per-child VDOM patches scoped to[dj-sticky-view="<id>"]via a newapplyPatches(patches, rootEl)variant in12-vdom-patch.js(whenrootElis non-null, node lookups / focus save-restore / autofocus queries all scope to that subtree).- Per-view VDOM version tracking via
clientVdomVersions: Map<view_id, number>with"__root"sentinel for top-level patches.
Client-side
static/djust/src/45-child-view.js—stickyStashMap;stashStickySubtrees()(detach on outbound nav),reconcileStickyHold(views)(drop non-authoritative),reattachStickyAfterMount()(replace[dj-sticky-slot]with stashed subtree viareplaceWith()— DOM identity preserved),handleStickyUpdate(msg)(scoped patch apply),clearStash()(abnormal-close cleanup).18-navigation.jscallsstashStickySubtrees()BEFORE outboundlive_redirect_mount(and beforepopstate-triggered redirects).03-websocket.jsonclose callsclearStash()on abnormal disconnect.[dj-root]audit across40-dj-layout.js,24-page-loading.js,12-vdom-patch.jsautofocus sites adds:not([dj-sticky-root])so sticky children don't masquerade as layout / page roots.
Security
- Per-sticky auth re-check via new
djust.auth.check_view_auth_lightweight(view, request) -> bool; a sticky view whose permissions are revoked mid-session is unmounted on the next navigation. DJUST_LIVE_RENDER_ALLOWED_MODULESprefix-allowlist gates dotted-path resolution (unset = permit-all, backward compatible).sticky_idHTML-escaped via server-sideescape()+CSS.escapeon client-side selectors.- Client stash bounded by developer-authored content; idempotent
stashStickySubtreescoalesces duplicates; cleared on abnormal WS close. - Inbound
sticky_update/sticky_holdframes rejected by the consumer's allowlist (server-to-client only).
Testing (32 Python + 20 JSDOM + 6 integration)
- 11 Phase A tests in
tests/unit/test_live_render_tag.py(HTML-parsed) + 21 Phase B/C tests intests/unit/test_sticky_preserve.py. - 7 Phase A tests in
tests/js/child_view.test.js+ 15 Phase B/C tests intests/js/sticky_preserve.test.js. - 3 end-to-end tests in
tests/integration/test_sticky_redirect_flow.py(Dashboard→Settings preservation, rapid A→B→A instance identity, no-slot reconcile path) + 3 demo-app smoke tests covering the full navigation cycle. - Phase C regression tests:
skipMountHtmlmount branch reattaches sticky subtrees (Fix F1);disconnect()drains_sticky_preservedso background tasks don't leak (Fix F2).
Documentation
- ADR-011 — wire protocol, DOM attributes,
client/server flow diagrams, full security model + threat matrix, failure
modes, relationship to v0.7.0
dj-activity. - User guide — quick start, common patterns, limitations, debugging, FAQ.
- Runnable demo app in
examples/demo_project/sticky_demo/— Dashboard, Settings, Reports pages with sticky AudioPlayer + NotificationCenter widgets showing preservation +no-slotunmount.
-
FLIP list-reorder animations (v0.6.0 animations milestone finale) — Opt-in per container via
dj-flip. Declarative attribute on a list parent animates direct-child reorders using First-Last-Invert-Play. Tunables:dj-flip-duration(default 300ms, parsed viaNumber+isFinite+ clamp[0, 30000]— trailing garbage rejects to fallback),dj-flip-easing(defaultcubic-bezier(.2,.8,.2,1), strings containing;"'<>rejected to defeat CSS-property-breakout). Respectsprefers-reduced-motion. Nested[dj-flip]isolated viasubtree: false. Author-specified inlinetransformon children is preserved across the animation. Overlapping reorders are guarded against cache corruption via an in-flight-transition check. Works with keyed lists where items carry stableid=(Rust VDOM emits MoveChild). Lands instatic/djust/src/44-dj-flip.js(~260 LOC). 12 JSDOM tests intests/js/dj_flip.test.js. -
{% djust_skeleton %}shimmer placeholder (v0.6.0 animations milestone finale) — Template tag for placeholder blocks. Props:shape(line|circle|rect, whitelist-validated),width/height(regex-whitelisted against^[\d.]+(px|em|rem|%|vh|vw|ch)?$, invalid falls back to shape default),count(clamped to[1, 100]),class_. All values HTML-escaped viabuild_tag(). Shimmer@keyframesemitted once per render viacontext.render_context. Integrates with existingdj-loadingshorthand and with{% if async_pending %}server blocks. 21 Python tests intests/unit/test_djust_skeleton_tag.py.
[0.5.7rc1] - 2026-04-23
Added
-
Resumable uploads across WebSocket disconnects (v0.5.7 — closes #821) — Long mobile uploads now survive network hiccups, backgrounded tabs, and brief WS drops. New
djust.uploads.resumable.ResumableUploadWriterwraps any existingUploadWriter(S3 MPU, GCS, Azure, tempfile) and persists chunk-level state into a pluggableUploadStateStore. Two stores ship in core:InMemoryUploadState(default, single-process) andRedisUploadState(requiresdjust[redis], multi-process / multi-host). New WS message{"type":"upload_resume","ref":X}returns{"type":"upload_resumed","status":"resumed|not_found|locked","bytes_received":N,"chunks_received":[...]}. New HTTP status endpointGET /djust/uploads/<upload_id>/status(session-scoped, cross-user probes blocked). Client-side IndexedDB cache in15-uploads.jslets tabs resume uploads after reload if the file reference can be re-selected. State is capped at 16 KB per upload_id (run-length-compressed chunk ranges) with 24-hour default TTL. Opt-in per slot:allow_upload("video", writer=S3Resumable, resumable=True). ~1,050 LOC net acrosspython/djust/uploads/(__init__.pymodified,resumable.py,storage.py,views.pyadded),python/djust/websocket.py,python/djust/static/djust/src/15-uploads.js(+03-websocket.jsdispatch), full wire-protocol spec + failure-mode + security analysis indocs/adr/010-resumable-uploads.md. 44 unit tests inpython/djust/tests/test_resumable_uploads_821.py(compaction, in-memory + fake-Redis roundtrip, writer lifecycle, resume resolution, TTL expiry via mock clock, concurrent-resume rejection, HTTP status view) plus 2 async WS handler cases in the same file, plus 9 JSDOM cases intests/js/upload_resume.test.js(file-hint fingerprint, UUID round-trip, IDB shim roundtrip, cleanup on complete). -
Upload writers — S3 pre-signed PUT URLs + first-class GCS/Azure backends (v0.5.7 — closes #820, #822) — New
djust.contrib.uploads.s3_presignedmodule lets clients upload directly to S3 via a pre-signed URL; djust only signs and observes completion via S3 event webhook. Newdjust.contrib.uploads.gcs.GCSMultipartWriteranddjust.contrib.uploads.azure.AzureBlockBlobWritership as first-classUploadWritersubclasses with consistent error taxonomy (UploadError,UploadNetworkError,UploadCredentialError,UploadQuotaError, re-exported fromdjust.uploads). Client-sidedjust.uploads.uploadPresigned(spec, file, hooks)streams bytes straight to object storage via XHR (progress viaxhr.upload.onprogress), bypassing the WS upload machinery. Optional extras:djust[s3],djust[gcs],djust[azure]. ~650 LOC + 50 regression tests (mocked SDKs) acrosspython/djust/tests/test_presigned_s3_820.py,python/djust/tests/test_gcs_upload_writer_822.py,python/djust/tests/test_azure_upload_writer_822.py. -
Docs cleanup: 4 issues closed — dj-remove no-CSS-transition gotcha (#902), dj-transition-group long-form precedence (#907), Django 5.1 + 5.2 classifiers in
pyproject.toml(#912), new guide page fordj-virtualvariable-height mode atdocs/website/guides/virtual-lists.md(#952). -
dj-virtual variable-height items via ResizeObserver — closes #797 — PR #796 shipped
dj-virtualwith fixed-height items only. This adds opt-in variable-height support via a newdj-virtual-variable-heightboolean attribute. Implementation: ResizeObserver per rendered item feeds aMap<index, number>height cache; a lazily-computed prefix-sum array drives offset math and the virtual spacer total. Unmeasured items fall back to a configurabledj-virtual-estimated-height(default 50px). Fixed-height mode (dj-virtual-item-height="N") is unchanged — tested explicitly as a regression guard. Updated29-virtual-list.js(~180 LOC net) and 4 new JSDOM cases intests/js/virtual_list.test.jscovering attribute activation, mixed-height prefix-sum math, RO-driven cache updates, and fixed-mode regression. -
Tooling: CHANGELOG test-count validator — closes #908 — new
scripts/check-changelog-test-counts.pyparses phrases likeN JSDOM cases,N regression tests,N unit tests,N test cases,N parameterized casesin the[Unreleased]section, resolves every backtickedtests/js/*.test.js/python/djust/tests/*.py/tests/unit/*.pypath inside the same bullet, counts test functions in each, and fails if the claim doesn't match reality. Delta phrases (2 new cases,3 additional tests) are deliberately skipped — they can't be verified without git history. Wired into.pre-commit-config.yamlas a local hook scoped to^CHANGELOG\.md$and exposed asmake check-changelog. Self-tested by 7 cases intests/test_changelog_test_counts.pycovering match/mismatch, JSDOM-vs-py file resolution, multi-file summing, delta ignore, and missing-section tolerance. -
Tooling: CodeQL triage script — closes #916 —
scripts/codeql-triage.sh [rule-id]paginates/repos/{owner}/{repo}/code-scanning/alerts?state=openviagh apiand emits a markdown triage doc grouped byrule.id, sorted within each group by file/line. Optional positional arg filters to a single rule for focused triage sessions. Turns the raw alert dump (noisy JSON) into something reviewable in a PR comment or a doc. Documented inscripts/README.md. -
Tooling: CodeQL sanitizer MaD model — closes #934 — new extension pack at
.github/codeql/models/(qlpack.yml +djust-sanitizers.model.yml) teaches CodeQL thatdjust._log_utils.sanitize_for_log()is a log-injection sanitizer. Referenced from.github/codeql/codeql-config.ymlvia a newpacks:section. Closes the class of false-positivepy/log-injectionalerts we've been dismissing individually. Verification lands with the next main-branch CodeQL scan. See.github/codeql/README.mdfor the tuple shape, fallback plan (hand-writtenLogInjectionFlowConfigurationoverride), and links to CodeQL's data-extensions docs. -
ADR-009: Mixin side-effect replay on WebSocket state restoration — closes #897 — formalizes the
_restore_<concept>()pattern first shipped ad-hoc in PRs #891 (UploadMixin, #889) and #895 (PresenceMixin- NotificationMixin, #893 / #894). Codifies the serialization contract
(JSON-only saved attrs), error handling (WARNING-level wrap, never
kill the WS), convergence/idempotency requirement, naming convention
(
_restore_<concept>), and call ordering inLiveViewConsumer. Documents the rejected alternatives: don't-skip-mount (perf cost), snapshot-entire-managers (serialization complexity), pickle-to-session (security + format stability). New file:docs/adr/009-mixin-side-effect-replay.md.
- NotificationMixin, #893 / #894). Codifies the serialization contract
(JSON-only saved attrs), error handling (WARNING-level wrap, never
kill the WS), convergence/idempotency requirement, naming convention
(
Fixed
-
Framework cleanup (closes #762, #890) — djust.A010 / A011 system checks now recognize proxy-trusted deployments: when
SECURE_PROXY_SSL_HEADER+DJUST_TRUSTED_PROXIESare both set,ALLOWED_HOSTS=['*']is accepted (supports AWS ALB, Cloudflare, Fly.io, and other L7 load balancers where task private IPs rotate). Also filters ~25 framework-internal attrs (sync_safe,login_required,template_name,http_method_names,on_mount_count,page_meta, etc.) fromLiveView.get_state(), the WS_snapshot_assignschange-detection path, and the_debug.state_sizesobservability payload — user's reactive state is no longer swamped by framework config. Non-breaking fix via a newlive_view._FRAMEWORK_INTERNAL_ATTRSfrozenset; attribute names unchanged. 14 new regression tests inpython/djust/tests/test_a010_proxy_trusted_890.pyandpython/djust/tests/test_get_state_filter_762.py. Deployment guide updated with the proxy-trusted escape-hatch pattern. -
JS-centric batch (closes #949, #951, #953) — tag_input hidden-input payload now JSON-encoded instead of comma-separated, so tag values containing commas round-trip intact (#949). dj-virtual variable-height cache now keyed by
data-keyattribute (configurable viadj-virtual-key-attr), falling back to index when absent — cached heights survive item reorders (#951). Consolidated JSDOM test helpers attests/js/_helpers.js(createDom,nextFrame,fireDomContentLoaded,makeMessageEvent,mountAndWait) and refactored 3 test files to use them (#953). 2 new Python regression tests (commas + quotes round-trip) and 3 new JSDOM cases (reorder survival, index fallback, custom key attribute). Guardrail added toscripts/build-client.shto fail fast iftests/js/_helpers.jsever leaks into the production bundle. -
Hygiene batch (closes #791, #794, #795, #818, #948) — bumped
ruff-pre-commitfrom v0.8.4 to v0.15.11 (#948) and appliedruff formatto all resulting drift (#791 — expanded beyond the original 5 files due to modern-ruff disagreements; 19 files total acrosspython/djust/andtests/). Addedlogger.debugnotice incomponents/suspense.pywhen{% dj_suspense await=X %}receives a non-AsyncResult value so a typo surfaces during development (#794), simplified a redundantor not value.okcheck nearsuspense.py:138given the AsyncResult mutually-exclusive-flag invariant (#795), wrapped the namespaceddata-hookattribute value withdjango.utils.html.escape()for defense-in-depth intemplatetags/live_tags.py(#818), and corrected stale test-count claims in two historical CHANGELOG bullets (test_assign_async.py11 → 18,test_suspense.py11 → 12) flagged by the #795 reviewer. No behavior change. -
Security + cleanup: pre-existing test failures, redirect audit, dep ceilings, edge tests — closes #910, #921, #922, #935 — #935: fixed 3 stale test assertions that were checking for leaked exception-class names in API error responses. The implementations in
api/dispatch.py,observability/views.pydeliberately sanitize error payloads (don't echoRuntimeError/ internal method names to clients; send to server logs instead). Tests now verify the sanitized contract ("server logs"inerror, handler_name / session_id echo) rather than the leaked details. Fixestest_api_response.py::test_dispatch_serialize_str_missing_method_returns_500,test_observability_eval_handler.py::test_eval_500_when_handler_raises, andtest_observability_reset_view.py::test_reset_500_when_mount_raises. #921: expanded open-redirect audit beyond PR #920 —mixins/request.pynow validateshook_redirectreturned by developer-definedon_mounthooks viaurl_has_allowed_host_and_scheme, falling back to"/"and logging a WARNING on unsafe targets.auth/mixins.pyLoginRequiredLiveViewMixin.dispatchnow validates the computed login URL as defense-in-depth against misconfiguredsettings.LOGIN_URL, falling back to"/accounts/login/". #922: 7 new regression tests inpython/djust/tests/test_security_redirects_paths.py—javascript:scheme rejection, HTTPS-to-HTTP downgrade, null-byte path-injection, uppercase/case-sensitive allowlist, hook_redirect off-site rejection, hook_redirect same-site acceptance, and off-siteLOGIN_URLfallback. #910: added upper-bound ceilings to all runtime + dev dependencies inpyproject.toml(e.g.requests>=2.28,<3,orjson>=3.11.6,<4,nh3>=0.2,<1). Prevents uncontrolled major bumps duringuv lockrefresh (see PR #909 which caught Django 6.x resolving under>=4.2). Ceiling policy documented in a comment above[project.dependencies]. Verified withuv lock— only material change isredis7.3 -> 6.4 (stays under new<7ceiling). -
UploadMixin defensive replay for schema-changed configs — closes #892 —
_restore_upload_configsnow wraps each per-slotallow_upload(**cfg)in try/exceptTypeError. On signature mismatch (kwarg added / renamed / removed between djust versions), logs a WARNING identifying the slot- the mismatched kwarg, then falls back to
allow_upload(slot_name)— bare-minimum replay — so uploads for that slot still work with default config. One broken saved dict no longer kills replay for every other slot on the page. Each saved dict is now tagged with_upload_configs_version = 1for future explicit migrations. Regression tests intests/unit/test_mixin_replay_schema_cross_loop_892_896.py.
- the mismatched kwarg, then falls back to
-
NotificationMixin cross-loop restore — closes #896 —
_restore_listen_channelsnow detects when thePostgresNotifyListenersingleton is stranded on a closed event loop (server restart with fresh ASGI loop, test harness per-test loops, sticky-session LB cross-worker handoff) and calls a newPostgresNotifyListener.reset_for_new_loop()classmethod to drop the singleton before replay. The pre-check inspectslistener._loop.is_closed(); a per-channelexcept RuntimeErrorbranch handles the race where the loop closes between the pre-check and theensure_listeningcall (resets and retries once). Prevents silent NOTIFY drops on cross-loop restore. Regression tests intests/unit/test_mixin_replay_schema_cross_loop_892_896.py. -
Observer JS — closes #879, #880, #881, #882 — #879:
37-dj-mutation.jsand38-dj-sticky-scroll.jsdocument-level root observers now detect attribute REMOVAL on already-observed elements (viaattributes: true+attributeFilter: ['dj-mutation']/['dj-sticky-scroll']) and call the module's teardown helper. Previously removing the attribute from an element left a staleMutationObserver+ scroll listener attached. #880: documented theMap-vs-WeakMapchoice in39-dj-track-static.js— the reconnect-diff iterates all tracked elements to compare snapshot URLs, andWeakMapdoes not support iteration; theisConnectedcheck in_checkStalehandles detached elements. #881: documented unconditional scroll-to-bottom on install in38-dj-sticky-scroll.js— matches Phoenix phx-auto-scroll / Ember scroll-into-view behavior (sticky-scroll is an "opt into bottom-pinning" attribute; authors want the initial view pinned to the most recent content: chat, log output). #882: regression test intests/js/dj_mutation.test.js— nodj-mutation-fireCustomEvent fires when the element is removed before the debounce timer expires (existing_tearDownDjMutationpath correctly clears the pending timer on removal).
Tests
- dj-transition-group follow-ups — closes #905, #906 —
#905 The VDOM
RemoveChildintegration test intests/js/dj_transition_group.test.jswaited 700 ms per run for the default dj-remove fallback timer. Pinneddj-remove-duration="50"on the child and reduced the wait to ~80 ms, dropping this file's wallclock from ~1.2 s to ~600 ms. #906 Added a nested-group regression test — outer + inner[dj-transition-group]parents each install their own per-parent observer (subtree:false), so a new child appended toinnergets the inner group's enter/leave specs and is not clobbered by the outer's. Pins the subtree-scoping invariant relied on by the phase-2c implementation.
Fixed
-
Mechanical cleanup — closes #914, #915 — #914: dropped redundant
ch == " "clause in_log_utils.sanitize_for_log— ASCII space is already printable so the explicit check was dead. #915: bulk-appliedruff format(pinned pre-commit version 0.8.4) to 4 pre-drifted files (3 theming test files +uploads.py) to bring them to canonical form. No behavior change in either fix. -
3 latent bugs caught by prior CodeQL-cleanup audits — closes #930, #932, #933 — #930 FormArrayNode inner content:
{% form_array %}...{% endform_array %}parsed the block body into a nodelist viaparser.parse(("endform_array",))butFormArrayNode.rendernever rendered that nodelist — users' inner template markup silently disappeared. Fixed by rendering the nodelist once per row withrow,row_index, andforloop(dict shape:{counter, counter0, first, last}) pushed onto the template context; empty or whitespace-only blocks keep the original single-input-per-row default output, so existing users see no change. #932 tag_input missingname=attribute:TagInput._render_customrendered a visible "type to add"<input class="tag-input-field" placeholder="...">with noname=, so form submissions silently dropped the tag list from POST data. Fixed by emitting a<input type="hidden" name="<self.name>" value="<csv of tags>">alongside the visible input wheneverself.nameis non-empty; hidden value ishtml.escape'd. #933 gallery/registry.py dead discover_* path:discover_template_tags()anddiscover_component_classes()were public helpers exported fromdjust.components.gallery.__init__butget_gallery_data()never called them — a developer adding a new@register.tagorComponentsubclass without updating the curatedEXAMPLES/CLASS_EXAMPLESdicts had that new thing silently missing from the rendered gallery. Fixed by wiring both helpers intoget_gallery_data()as a cross-check: any registered tag / component class missing an example entry emits alogger.debugwarning naming the missing entries, and discovery failures are caught so the gallery never breaks at runtime. 14 regression tests acrosspython/djust/tests/test_form_array_930.py,python/djust/tests/test_tag_input_932.py,python/djust/tests/test_gallery_registry_933.py(7 of which fail on main pre-fix; 2 added later under #949 for commas-in-values round-trip). No behavior change for non-broken inputs. (python/djust/components/templatetags/djust_components.py,python/djust/components/components/tag_input.py,python/djust/components/gallery/registry.py) -
dj-remove follow-ups — closes #900, #901 — Extracted shared
_teardownState(el, state)helper in42-dj-remove.jsso_finalizeRemovaland_cancelRemovalno longer duplicate the clearTimeout + removeEventListener + observer.disconnect + _pendingRemovals.delete block (Stage 11 nit from PR #898). Added a debug warning (gated onglobalThis.djustDebug) when_parseRemoveSpecencounters a 2-token value likedj-remove="fade-out 300"— previously silent fall-through. 2 new JSDOM regression cases intests/js/dj_remove.test.js(12/12 passing). -
dj-transition edge cases — closes #886, #887, #888 — #886
_parseSpecin41-dj-transition.jsnow rejects comma, paren, and bracket separators up front (returnsnulland emits a debug warning gated onglobalThis.djustDebug) instead of lettingclassList.addthrowInvalidCharacterErrorat runtime — matches the dj-remove #901 loud-in-debug / silent-in-prod pattern. #887 Thecleanupcallback (bothtransitionendhandler and 600 ms fallback path) now guards withel.isConnected— if the element has been detached from the DOM before cleanup fires, we skip classList and listener work and just drop the_djTransitionStateentry. Prevents any futureparentNode.Xaccess from NPE'ing on a detached node. #888 Unskipped the two previously-flakytransitionendtests intests/js/dj_transition.test.jsby swapping timing-sensitivesetTimeout(..., 30)waits for synchronousel.dispatchEvent(new Event('transitionend'))— deterministic under vitest parallel load. Added one new test covering the #886 parser rejection path. All 9 dj-transition tests pass deterministically.
[0.5.6rc1] - 2026-04-23
BREAKING CHANGES
- Dropped Python 3.9 support (
requires-python = ">=3.10"). Python 3.9 reached end-of-life on 2025-10-05; the ecosystem has since moved on (orjson, pytest, python-dotenv, requests, and mcp have all dropped py3.9 support in versions that carry security fixes). Keeping py3.9 in therequires-pythonconstraint kept 4 Dependabot alerts stuck open against the py3.9 resolution train — alerts which had no upstream patch available on py3.9. Closes Dependabot alerts #41 (orjson recursion DoS), #87 (pytest tmpdir race), #89 (python-dotenv symlink follow inset_key), #62 (requests insecure temp file reuse). Existing py3.9 users can continue installing djust v0.5.x from PyPI; v0.5.6+ requires py3.10+. Also bumped[tool.ruff] target-versiontopy310and[tool.mypy] python_versionto3.10; collapsed the orjson / mcp conditional pins (previously carried a py3.9-stuck floor).
Added
-
dj-remove— exit animations before element removal (v0.6.0) — PhoenixJS.hide/phx-removeparity. When a VDOM patch, morph loop, ordj-updateprune would physically remove an element carryingdj-remove="...", djust delays the actualremoveChild()until the CSS transition the attribute describes has played out (or a 600 ms fallback timer fires, overridable viadj-remove-duration="N"). Two forms: three-tokendj-remove="opacity-100 transition-opacity-300 opacity-0"matches thedj-transitionshape (start → active → end), and single-tokendj-remove="fade-out"applies one class and waits fortransitionend. If a subsequent patch strips thedj-removeattribute from a pending element, the pending removal cancels and the element stays mounted. Public hookwindow.djust.maybeDeferRemoval(node)is called from five removal sites in12-vdom-patch.js. Descendants of a[dj-remove]element are NOT independently deferred — they travel with their parent, matching Phoenix. Newstatic/djust/src/42-dj-remove.js. 10 JSDOM cases intests/js/dj_remove.test.js. Phase 2a of the v0.6.0 Animations & transitions work; FLIP /dj-transition-group/ skeletons remain separate follow-ups. -
dj-transition-group— orchestrate enter/leave animations for child lists (v0.6.0) — React<TransitionGroup>/ Vue<transition-group>parity. Authors mark a parent container and specify enter + leave specs once; djust wires those specs onto each child by settingdj-transition(enter) anddj-remove(leave) — re-using the already-shipped phase-1 / phase-2a runners (#885 / #898) rather than re-implementing the phase-cycling or removal-deferral machinery. Two forms: shortdj-transition-group="fade-in | fade-out"(pipe-separated halves, each accepting the same 1- or 3-token shape asdj-transition/dj-remove), and long form with baredj-transition-groupplusdj-group-enter/dj-group-leaveon the parent. Initial children get the leave spec only by default (so they animate out if later removed, but nothing animates in on first paint); opt them into first-paint enter animation viadj-group-appearon the parent. Never overwrites author-specifieddj-transitionordj-removeon a child — escape hatch for per-item overrides. A per-parentMutationObserverpicks up newly appended children; a document-level observer handles parents that arrive via VDOM patch or attribute mutation. Newstatic/djust/src/43-dj-transition-group.js. 11 JSDOM cases intests/js/dj_transition_group.test.jscover short-form parsing, invalid input, manual_handleChildAdded, respect for pre-existing per-child attrs, default leave-only initial wiring,dj-group-appearenter opt-in, post-mount append via observer,_uninstalldisconnecting the per-parent observer, parent-removal auto-cleanup via the root observer, end-to-end VDOMRemoveChilddeferral through the wireddj-remove, and cancel-on-strip uninstalling the per-parent observer whendj-transition-groupis removed at runtime (symmetric withdj-remove). Phase 2c of the v0.6.0 Animations & transitions work; FLIP and skeletons remain separate follow-ups. (python/djust/static/djust/src/43-dj-transition-group.js)
Fixed
-
Code-scanning cleanup: remaining ~35
py/cyclic-importnotes + 7 misc note-level alerts. Real refactor: extractedContextProviderMixinfromlive_view.pyto a new_context_provider.pymodule socomponents/base.pycan import it without creating a module-level cycle back throughlive_view -> serialization -> components/base.live_view.pyre-exportsContextProviderMixinfor back-compat (existing user code importingfrom djust.live_view import ContextProviderMixinkeeps working). Closes 3 real cyclic-import alerts (#2112, #2113, #2114). The remaining ~28 theming cyclic-import notes (inmanager.py,registry.py,theme_css_generator.py,pack_css_generator.py,theme_packs.py,manifest.py,css_generator.py) are allfrom ... importstatements INSIDE function bodies (or the module-level counterpart paired with such a lazy import) — deliberate cycle breakers where the runtime module graph is acyclic — dismissed with specific justification. Also fixed 3py/mixed-returnsvia mechanical cleanup:theming/inspector.py(added 405 Method-Not-Allowed fallback),admin_ext/views.py(replaced barereturnwithreturn Noneinrun_action),management/commands/djust_audit.py(explicitreturn Nonefrom allhandle()branches). Dismissed 3py/unused-global-variablefalse positives (lazy-init cache pattern incomponents/icons.py:_icon_sets_cache,theming/theme_packs.py:_theme_imports_done,observability/log_handler.py:_installed_handler— same pattern as_psycopgdismissed in #2104/#2105) and 1py/ineffectual-statementfalse positive (tutorials/mixin.py:371—await corois a real async effect, not an ineffectual expression). No behavior change; full Python suite passes (3428 passed, 15 skipped). (python/djust/_context_provider.py,python/djust/live_view.py,python/djust/components/base.py,python/djust/theming/inspector.py,python/djust/admin_ext/views.py,python/djust/management/commands/djust_audit.py) -
Cleanup: 36
py/empty-except+ 6 misc CodeQL note-severity alerts — Narrowed over-broadexcept Exception: passto specific exception types where the call surface was knowable, and addedlogger.debug(...)(withimport logging; logger = logging.getLogger(__name__)where not already present) for optional-feature probes incomponents/gallery/views.py(optionaldjust_themingstatic CSS link),components/icons.py(optionalDJUST_COMPONENTS_ICON_SETSsetting),auth/admin_views.py(optionaldjango-allauthOAuth stats, 2 sites),auth/djust_admin.py(optional allauth registry), andmixins/context.py(best-effort descriptor resolution). Annotated "skip invalid numeric input" sites with justification comments (+pass→continuefor clarity) acrosscomponents/templatetags/_charts.py(4),components/rust_handlers.py(8),components/components/{calendar_heatmap,heatmap,line_chart,source_citation}.py,components/descriptors/carousel.py,components/function_component.py(2),components/mixins/data_table.py(3),components/templatetags/djust_components.py(2), and similar narrow/intentional catches inchecks.py,components/base.py(optional@event_handlerdecoration),mixins/waiters.py(idempotent waiter removal),observability/dry_run.py(best-effort bulk-op count),theming/management/commands/djust_theme.py, andtheming/templatetags/theme_tags.py. Re-export incomponents/templatetags/djust_components.py(_get_field_type,_infer_columns,_queryset_to_rowsfrom_forms) made explicit via__all__(closespy/unused-import#2171). Deleted 3 JS unused-variable declarations:decoderincomponents/static/djust_components/ttyd/ttyd_terminal.js:35,resolvedModeintheming/static/djust_theming/js/theme.js:416, andgetCookie()intheming/static/djust_theming/js/theme.js:449. Dismissed 2py/unused-global-variablefalse positives (#2104, #2105 —_psycopg/_psycopg_sqlindb/notifications.pyare lazy module-level caches assigned viaglobalinside_ensure_psycopg(); CodeQL's scope analyzer doesn't track global-write patterns). 4 note-levelpy/cyclic-importalerts (#2096, #2112-#2114) left for scanner rescan — expected to auto-close as PR #928's refactor propagates. No behavior change; full Python suite passes (3428 passed, 15 skipped). -
Code-quality cleanup — ~66 CodeQL note-severity alerts — mechanical fixes: deleted unused imports (treated re-exports with
__all__+# noqa: F401preservation; replaced side-effect submodule imports withimportlib.import_module), removed ~30 unused local variables acrossrust_handlers.py,templatetags/djust_components.py,components/*.py, andtemplatetags/_forms.py/_advanced.py, removed ~4 unused module-level names (default_app_configincomponents/__init__.py,theming/__init__.py,admin_ext/__init__.py— obsolete since Django 3.2 auto-discovery), simplified 3lambda vals: f(vals)wrappers inAGG_FUNCS(pivot-table aggregations) to baresum/len, deduped 2import json/import asynciooccurrences infunction_component.py/mixins/data_table.py/db/notifications.py, reconciledimport X+from X import Yconflicts ingallery/registry.pyandtemplatetags/djust_components.py, and removed ineffectual single-...statements in Protocol / abstract method bodies inapi/auth.pyandtenants/audit.py. No behavior change; full suite passes (3428). Plus 3 dismissed with justification: 2 ×py/catch-base-exceptioninasync_work.py(existing# noqa: BLE001comments + documented design intent of surfacing every failure viaAsyncResult.errored), and 1 ×js/syntax-errorontheming/templates/.../theme_head.html(CodeQL's JS analyzer erroneously parsing a Django template as JavaScript). -
Break
themes → _base → presets/theme_packscyclic import (873 CodeQL alerts) + add explicitevent.origincheck to service workermessagehandler — CodeQL'spy/unsafe-cyclic-importrule flagged 872 alerts across the theming subsystem:themes/_base.pyimported dataclasses + shared style instances from..presetsand..theme_packs, and those two modules re-imported each theme file under.themes.*at module load — a real cycle that happened to work only becauseColorScale/ThemeTokens/ etc. were defined earlier inpresets.pythan the theme imports. Extracted the pure data into two new dependency-free modules:python/djust/theming/_types.py(14 dataclass types:ColorScale,ThemeTokens,SurfaceTreatment,ThemePreset,TypographyStyle,LayoutStyle,SurfaceStyle,IconStyle,AnimationStyle,InteractionStyle,DesignSystem,PatternStyle,IllustrationStyle,ThemePack— stdlib imports only) andpython/djust/theming/_constants.py(~60 shared style instances —PATTERN_*,ILLUST_*,ICON_*,ANIM_*,INTERACT_*at both the design-system and pack levels; depends only on_types).themes/_base.pynow imports from those two modules, bypassing the cycle;presets.pyandtheme_packs.pyimport from the same new modules and re-export every type and instance under__all__for full backward compat (no theme author touches any import site). Also resolved the pre-existing shadow between twoInteractionStyleclass definitions (the narrow DS-levelInteractionStyleattheme_packs.py:150was silently shadowed by the wider pack-level one at:1374— allINTERACT_*module-level instances relied on fields only the wider class had; unified on the superset definition in_types.py) and theINTERACT_MINIMAL/INTERACT_PLAYFULname collision between the DS-level and pack-level bindings (kept the distinct runtime bindings via_INTERACT_MINIMAL_DS/_INTERACT_PLAYFUL_DS). Also tightened the service-workermessagehandler inpython/djust/static/djust/service-worker.jswith an explicitevent.origin !== self.location.originearly return at the top of the listener, satisfying CodeQL'sjs/missing-origin-checkrule (alert #2170 — follow-up to the source+scope check shipped in #925). 7 regression cases inpython/djust/tests/test_theming_imports_backcompat.pycover: presets/theme_packs type exports still importable, shared instance exports still importable,_basere-exports identical object identity topresets/theme_packs, per-theme files (vercel used as smoke) still construct a full triple, lazy theme-pack registry still populates 71 packs + 73 design systems, and the DS-vs-packInteractionStyledistinction forminimal/playfulis preserved (DSlink_hover="underline", packbutton_click="ripple"— both bindings round-trip). Expected alert closure: 872 ×py/unsafe-cyclic-import+ 1 ×js/missing-origin-check= 873. (python/djust/theming/_types.py,python/djust/theming/_constants.py,python/djust/theming/presets.py,python/djust/theming/theme_packs.py,python/djust/theming/themes/_base.py,python/djust/static/djust/service-worker.js) -
Dead conditional in
djust/theming/templatetags/theme_form_tags.py— the label-visibility check at line 88 hadisinstance(field.widget, template.library.InvalidTemplateLibrary if False else type(None)). Theif False else type(None)ternary always evaluated totype(None), making the first operand unreachable dead code (CodeQLpy/constant-conditional-expression). Dropped the dead branch; the isinstance check is nowisinstance(field.widget, type(None))with a comment explaining the intent. -
Close 21
py/undefined-exportCodeQL alerts —djust/auth/__init__.pyanddjust/tenants/__init__.pyuse a__getattr__-based lazy-import dispatcher to defer Django-ORM-dependent imports. CodeQL's static analysis doesn't recognize this pattern; names declared in__all__but only resolved via__getattr__were flagged. Added aTYPE_CHECKINGblock to each__init__.pywith eager import statements gated behindif TYPE_CHECKING:— the imports execute only under static analysis (mypy, CodeQL, IDEs), never at runtime. The lazy-import runtime behavior is unchanged. Newpython/djust/tests/test_lazy_import_resolution.py(47 parameterized cases) regression-tests that every__all__entry resolves. -
3 real bugs caught by CodeQL scanning (6 alerts closed) —
python/djust/components/gallery/views.py(py/stack-trace-exposure, 2 alerts): the gallery's per-variant render fallback interpolated the rawExceptionrepr into the HTML returned to the user (f'<div ...>Render error: {exc}</div>'), leaking internal template / class paths and error detail to any gallery viewer. Fixed to log vialogger.exception(...)and return a genericRender error — see server logsmessage at both thetype == "tag"template-render path and thetype == "class"render-callable path.python/djust/theming/build_themes.py(py/call-to-non-callable, 1 alert):BuildTimeGenerator.__init__assigned thegenerate_manifest: boolconstructor argument ontoself.generate_manifest, which shadowed the method of the same name atdef generate_manifest(self, generated_files). Callingself.generate_manifest(generated_files)at line 521 frombuild_all()would have raisedTypeError: 'bool' object is not callableon any invocation of the full build — the method was effectively unreachable. Renamed the attribute toself._generate_manifest(underscore = internal flag), updated the single consumer inside the method to match; the callable is now callable again.python/djust/theming/accessibility.py(py/str-format/missing-named-argument, 3 alerts):AccessibilityValidator.generate_accessibility_report_htmlpassed an HTML+CSS string throughstr.format(**kwargs)where the embedded literal CSS braces (body { font-family: ... }) were being parsed by Python's format machinery as placeholder keys, raisingKeyError/ValueErrorat runtime on the very first{it hit. Refactored to keep the CSS in a separate un-formatted string (_css_styles) and feed it as a single{styles}placeholder into the HTML template (_html_template); no double-brace escaping hazard, template semantics preserved. 4 regression cases inpython/djust/tests/test_codeql_bugfixes.pycover: exception-message not reflected in either gallery render fallback;generate_manifest(True)calls the method (no TypeError);generate_manifest(False)short-circuits to""; HTML report renders end-to-end with both<!DOCTYPE html>and surviving CSSfont-familytokens. (python/djust/components/gallery/views.py,python/djust/theming/build_themes.py,python/djust/theming/accessibility.py)
Security
-
Client-side markdown preview: escape user input before markdown transforms — closes 1 CodeQL
js/xss-through-domalert (#1978, warning) —inlineFormatinpython/djust/components/static/djust_components/markdown-textarea.jsapplied regex-based markdown substitutions on raw user input and wrote the result into the preview pane viainnerHTML, so a user typing# <script>alert(1)</script>into their textarea saw the raw<script>tag rendered in their own preview. Self-XSS in most deployments, but propagates to other users wherever a textarea'sdata-rawpayload later lands in another user's view (shared drafts, admin review screens, collaborative editors). Fix: callescapeHtml()at the top ofinlineFormat(before any regex transform — the markdown syntax chars*,_,`,[,],(,)are not in the escape set so the substitutions still match). Added_sanitizeUrl()that rewritesjavascript:,data:, andvbscript:URL schemes (case-insensitive, leading-whitespace tolerant) to#in link targets, closing the[click](javascript:alert(1))attack surface. 11 JSDOM regression cases intests/js/markdown_textarea_xss.test.jscover<script>/<img onerror>/<b>escaping in headings / paragraphs / lists, preserved**bold**/*italic*/`code`functionality,javascript:/data:/VBScript:URL rewriting, safehttps://and relative URLs preserved, and fenced-code-block escaping still works. (python/djust/components/static/djust_components/markdown-textarea.js) -
Service worker
postMessagesame-origin source check — closes 1 CodeQLjs/missing-origin-checkalert (#2106, warning) —python/djust/static/djust/service-worker.jsprocessed any incomingmessageevent without inspectingevent.source. Service workers are inherently same-origin (they cannot be loaded cross-origin, sopostMessagefrom a cross-origin page can't reach the SW), but defense-in-depth: a compromised same-origin frame outside the SW scope could still reach the handler. Fix: two-layer gate before touchingevent.data— (1) reject messages whoseevent.sourceis missing or whoseevent.source.typeis not'window'(rejectsworker/sharedworkerclients we don't expect), (2) reject WindowClient sources whoseurldoesn't start withself.registration.scope. 4 new JSDOM regression cases intests/js/service_worker.test.js(newdescribeblock "message origin check") cover no-source rejection, non-WindowClient rejection, out-of-scope URL rejection, and valid-WindowClient acceptance. Existing 12 SW tests unchanged — the pre-existing harness was updated to back-filltype: 'window'+ a scope-validurlon caller-supplied source objects, preserving the exact inputs each test verifies. (python/djust/static/djust/service-worker.js) -
Open-redirect + path-traversal hardening + dismiss
py/clear-text-*CodeQL false-positives (7 alerts closed/dismissed) — Real (3 code fixes, closing 4 alerts):python/djust/auth/views.pySignupView.get_success_urlaccepted anynextPOST param and passed it straight toredirect(), so a crafted form post could bounce newly-authenticated users to an attacker-controlled host — fixed by validating with Django'surl_has_allowed_host_and_scheme()against the current request host (withrequire_https=self.request.is_secure()); off-site, protocol-relative (//evil.com), and scheme-different values all fall back tosettings.LOGIN_REDIRECT_URL.python/djust/admin_ext/views.py:admin_login_requiredinterpolatedrequest.pathdirectly into the login-redirect query string (?next=<path>), letting a path containing&/#/ encoded control chars smuggle extra query params into the redirect — fixed withurllib.parse.urlencode({"next": request.path}).python/djust/theming/gallery/storybook.py:get_component_template_sourcejoined an HTTP-accessiblecomponent_nameURL kwarg into_COMPONENTS_DIR / f"{name}.html"with no validation — fixed with an allowlist regex^[a-z0-9_-]+$plus a resolved-path-under-base check so traversal payloads (../../../etc/passwd,../secret,foo/bar) return""instead of reading outside the components directory. False-positives (4 dismissed):py/clear-text-storage-sensitive-data+py/clear-text-loggingalerts trace taint fromMEDICAL_THEME/LEGAL_THEMEconstant imports intheming/presets.py— CodeQL's healthcare-PII heuristic matches the word "medical" / "legal" as identifiers, but the tainted values are CSS theme names (palette tokens, radii, font stacks), not healthcare or legal data. Dismissed on GitHub with "won't fix" and justification. 5 regression cases inpython/djust/tests/test_security_redirects_paths.pycover off-site / same-site / protocol-relative redirect outcomes plus path-traversal rejection and known-valid component name round-trip. (python/djust/auth/views.py,python/djust/admin_ext/views.py,python/djust/theming/gallery/storybook.py) -
Drop exception messages from API error responses — closes 8-10 CodeQL
py/stack-trace-exposurealerts — Stack traces and exception messages can reveal internal file paths, local variable names, DB schema details, and dependency versions, giving attackers a head-start on probing. Three call sites were rewritten to return generic messages and log the full traceback server-side vialogger.exception()instead of echoingstr(e)/type(e).__name__: {e}back in the JSON response body.python/djust/theming/inspector.py(3 sites attheme_inspector_apiGET/POST +theme_css_api) — these endpoints are publicly accessible with no access gating, so this is real prod exposure.python/djust/observability/views.py(4 sites atreset_view_statemount failure,eval_handlerinvalid-JSON body,eval_handlerTypeError,eval_handlercatch-all) — DEBUG-gated dev tools, but CodeQL still flags the response content; consistent generic-message pattern closes the alerts and the full trace is still captured in the standard log stream.python/djust/api/dispatch.py:384— theserialize_errorpath'sstr(exc)dropped in favor of the same generic message the sibling"handler_error"/ catch-all"serialize_error"branches already use. Addedlogger = logging.getLogger(__name__)to the two files that lacked one. 3 regression cases inpython/djust/tests/test_stack_trace_exposure.pyverify the sentinel exception message is not reflected in the response body. Two alerts onpython/djust/components/gallery/views.py:726,762share the reflective-XSS cookie-flow surface cleared by PR #918 and may auto-close on rescan; if they don't, dismiss-with-justification is appropriate (allowlist-validated values,escape()already applied). (python/djust/theming/inspector.py,python/djust/observability/views.py,python/djust/api/dispatch.py) -
Escape user input in gallery 404 responses & theme option fragments — closes 6 CodeQL
py/reflective-xssalerts (error severity) — Three real reflective-XSS sites inpython/djust/theming/gallery/views.py(lines 276, 281, 306):storybook_detail_viewandstorybook_category_viewechoed the user-controlled URL kwargscomponent_name/categoryintoHttpResponseNotFound(f"Unknown ...: {value}")withContent-Type: text/html, so a visitor hitting/storybook/<script>alert(1)</script>/got the raw payload reflected in the 404 body. Fix: wrap the interpolations withdjango.utils.html.escape(). Three defense-in-depth sites inpython/djust/components/gallery/views.py(lines 677, 726, 762 via_resolve_theme): cookie values (gallery_ds,gallery_preset) flow through an allowlist validator before being interpolated into<option>fragments, so the genuine attack surface is zero — but CodeQL's taint analyzer doesn't recognize the allowlist pattern. Addedescape()on the cookie-derived values' HTML interpolation sites; on validated input this is a no-op (allowlist values are plain ASCII identifiers), and it clears the taint flag for the static analyzer. 4 regression cases inpython/djust/tests/test_gallery_xss.pycover both the real-XSS 404 body escaping and the allowlist + escape behavior for malicious cookie values. (python/djust/theming/gallery/views.py,python/djust/components/gallery/views.py) -
Sanitize user-controlled values in log calls — closes 9 CodeQL
py/log-injectionalerts — Addeddjust._log_utils.sanitize_for_log(): strips CR/LF/TAB/control chars, replaces with?, truncates to 200 chars, always returns a string (None / non-string inputs become theirrepr). Applied at 5 call sites inpython/djust/api/dispatch.py(wrappingview_slug,handler_name) andpython/djust/theming/gallery/component_registry.py(wrappingcomponent_name,str(exc)) — the sites where HTTP request data flows intologger.exception/logger.debugcalls. Format strings unchanged; djust already uses%s-style lazy logging per CLAUDE.md. 8 unit tests inpython/djust/tests/test_log_sanitization.py. No behavior change for non-malicious input. -
Refresh
uv.lockto pull in CVE-fix versions for 8 packages — Addresses 23 open Dependabot alerts (13 unique CVEs). Bumps: Django 4.2.29 → 5.2.13 (CVE floor 4.2.30; tightenedpyproject.tomlceiling to<6to keep the major-version jump out of a security-only PR), cryptography 46.0.5 → 46.0.7 (buffer overflow + DNS name constraints), orjson 3.11.5 → 3.11.8 (deep-recursion DoS, floor 3.11.6), requests 2.32.5 → 2.33.1 (insecure temp-file reuse, floor 2.33.0), Pygments 2.19.2 → 2.20.0 (GUID-matching ReDoS), pytest 8.4.2 → 9.0.3 (tmpdir vulnerability), black 25.11.0 → 26.3.1 (arbitrary file writes from unsanitized cache input, dev-only), python-dotenv 1.2.1 → 1.2.2 (symlink following inset_key). Full Python test suite passes (3428 cases); full JS suite passes (1264 cases). No app code or test changes; lockfile +pyproject.tomlDjango ceiling only. Also catchesCargo.lockup to the v0.5.5rc1 crate versions (stale at 0.5.3rc1 on origin/main).
Changed
- Drop
blackdev dependency;ruff formatis now the canonical formatter — Pre-commit config has usedruff+ruff-formathooks since v0.5.x; noMakefile/ CI / import site references black. Removedblack>=24.10.0/black>=26.3.1from thedevgroup inpyproject.tomland the[tool.black]config section. Ruff already has matchingline-length = 100andtarget-version = "py39". Permanently closes the DependabotblackCVE alert on the Python 3.9 resolution train (black 26.x dropped 3.9 so that alert couldn't be patched; dropping black removes the surface entirely).
[0.5.4rc1] - 2026-04-22
Fixed
-
PresenceMixin+NotificationMixin— side-effect replay on WS state restoration (#893, #894) — Sibling bugs to #889, found via audit after theUploadMixinfix shipped in #891. Both issues share the same root cause: a mixin'smount()-called method has a process-wide side effect beyond setting instance attrs, and the WS consumer's state-restoration path (which skipsmount()) never re-issues the side effect. #893 (Presence):track_presence()callsPresenceManager.join_presence(...)as a per-process singleton registration; after restore, the restored user's presence is invisible to other users andhandle_presence_joindoesn't fire for the user's own join. #894 (Notifications):listen(channel)callsPostgresNotifyListener.instance().ensure_listening(channel)which issues the PostgresLISTEN channelSQL statement on the current process; after a cross-process restore (server restart between HTTP and WS, sticky-session LB routing WS to a different worker, worker reshuffle under load) the destination process's listener has no subscriptions, so NOTIFYs never reach the restored view. Fix (mirrors PR #891):PresenceMixin._restore_presence()replaysjoin_presencewhen_presence_tracked=True;NotificationMixin._restore_listen_channels()replaysensure_listeningper channel (both convergent under replay —PresenceManager.join_presenceoverwrites the existing record with identical data so repeated calls are a no-op in effect;ensure_listeningexplicitly early-returns on known channels); WS consumer's state-restoration path calls both right after_restore_private_state(), alongside the existing_restore_upload_configs()call. All three methods are defensive: missing attributes / backend errors / per-item failures are logged at WARNING and swallowed — restoration must never kill the WebSocket. 11 regression cases intests/unit/test_mixin_restoration_893_894.pycover both mixins' happy paths, no-op guards (not-tracked, missing user_id, empty channel set, missing attribute), exception handling (backend exception, per-channel failure, postgres unavailable), and an end-to-end session-round-trip test. (python/djust/presence.py,python/djust/mixins/notifications.py,python/djust/websocket.py) -
UploadMixin— uploads broken after HTTP→WS state restoration (#889) — Production-critical bug affecting every app usingUploadMixinwith the default pre-rendered HTTP→WS flow. The WS consumer's state-restoration path (websocket.py:1540-1572) skipsmount()when pre-rendered session state exists, and the liveUploadManagerinstance isn't JSON-serializable — so_upload_managersilently dropped by_get_private_state(), never restored, and any upload request hit_handle_upload_registerwith"No uploads configured for this view". Fix:allow_upload()now also records each call as a JSON-serializable dict inself._upload_configs_saved(list of kwarg dicts with primitive values); the newUploadMixin._restore_upload_configs()method replays the saved calls; the WS consumer calls it at the end of the state-restoration path (right after_restore_private_state). Result: restored views behave identically to fresh-mount views. Caveat:allow_upload(writer=CustomWriterClass)— the writer class itself still can't round-trip through JSON; a warning is logged at replay time and the config falls back to the default buffered writer. Apps that rely on custom writers with session restoration need a follow-up design (out of scope for this fix). 10 regression cases intests/unit/test_upload_restoration_889.pycover: call-list recording, writer-marker flag, multi-slot tracking, JSON round-trip survival, manager rebuild from the list, no-op on empty / missing list, idempotency across repeated restores, writer-fallback warning, and a full HTTP→session→WS-restore end-to-end scenario. (python/djust/uploads.py,python/djust/websocket.py)
Added
dj-transition— declarative CSS enter/leave transitions (v0.6.0) — PhoenixJS.transitionparity. Three-phase class orchestration so template authors can drive CSS transitions without writing adj-hook. Attribute value is three space-separated class tokens — phase 1 (start) applied synchronously, phases 2 (active) + 3 (end) applied on the next animation frame so the browser commits the start layout before the transition begins.transitionendremoves the active class (phase 3 stays as the final-state). 600 ms fallback timeout covers thedisplay: none/ zero-duration corner cases wheretransitionendnever fires. Any attribute-value change re-runs the sequence so authors can retrigger from JS. Newstatic/djust/src/41-dj-transition.js(~120 LOC); document-level MutationObserver matches thedj-dialog/dj-mutation/dj-sticky-scrollregistration pattern. 7 JSDOM cases intests/js/dj_transition.test.jscover spec parsing, phase-1 synchronous application, next-frame phase-2/3 application, transitionend cleanup, fallback-timeout cleanup, global export, and re-trigger-on-attribute-change. This is phase 1 of the v0.6.0 Animations & transitions work; FLIP,dj-remove,dj-transition-group, and skeleton components will ship as separate follow-ups. (python/djust/static/djust/src/41-dj-transition.js)
[0.5.3rc1] - 2026-04-22
Added
- Runtime layout switching —
self.set_layout(path)(v0.6.0) — Phoenix 1.1 parity. An event handler can swap the surrounding layout template (nav, sidebar, footer, wrapper markup) without a full page reload: inner state — form values, scroll position, focused element,dj-hookbookkeeping, third-party-widget references — is fully preserved because the live[dj-root]element is physically moved from the current body into the new layout rather than re-created. Server side: newLayoutMixininpython/djust/mixins/layout.pycomposed into theLiveViewbase, queuing at most one pending path (last-write-wins). WebSocket consumer: new_flush_pending_layout()wired at all nine_flush_page_metadatacall sites; renders the layout template with the view's currentget_context_data()and emits a{"type": "layout", "path": ..., "html": ...}frame. Graceful degradation:TemplateDoesNotExistor any render exception logs a warning and leaves the WS intact. Client side: newstatic/djust/src/40-dj-layout.jsmodule registered for thelayoutWS frame — finds the[dj-root]/[data-djust-root]inside the incoming HTML, splices in the live root node, swapsdocument.body, and fires adjust:layout-changedCustomEvent ondocument. Handles missing-root payloads and empty HTML gracefully. Tests: 12 Python cases intests/unit/test_layout_switching.py(mixin, consumer emit/noop/missing-template/no-mixin/view-none, LiveView composition) + 6 JSDOM cases intests/js/dj_layout.test.js(root-identity preservation, CustomEvent dispatch, malformed-payload refusal, empty-html noop,[dj-root]fallback, global export). Full user guide atdocs/website/guides/layouts.md(linked from_config.yamlandindex.md). Known limitation:<head>tags are not merged — if a layout needs new stylesheets, add them to the initial layout's<head>. (python/djust/mixins/layout.py,python/djust/mixins/__init__.py,python/djust/live_view.py,python/djust/websocket.py,python/djust/static/djust/src/03-websocket.js,python/djust/static/djust/src/40-dj-layout.js)
[0.5.2rc1] - 2026-04-22
Added
-
WebSocket per-message compression toggle —
DJUST_WS_COMPRESSION(v0.6.0) — VDOM patches compress extremely well (repetitive HTML fragments + JSON structure → 60-80 % wire-size reduction via zlib). Uvicorn and Daphne both negotiatepermessage-deflatewith browsers out of the box, so the wire-level compression is already free in most deployments — this change adds the declarative config toggle + documentation so operators can verify it's active, reason about the ~64 KB per-connection zlib context cost, and disable it cleanly on extreme-connection-density deployments or when running behind a compressing CDN. Newwebsocket_compressionconfig key (defaultTrue) exposed viadjust.config.config, bridged from a top-levelsettings.DJUST_WS_COMPRESSIONfor discoverability, and surfaced to the injected client bootstrap aswindow.DJUST_WS_COMPRESSION(application code can branch on it to skip manualJSON.stringifyoptimizations that only help without wire-level compression). 6 tests intests/unit/test_ws_compression_config.pycover default, override to True/False, truthy/falsy coercion, and client-script emission. Deployment guide (docs/website/guides/deployment.md) gains a new "WebSocket per-message compression" section covering the memory tradeoff, CDN double-compression footgun, and Uvicorn/Daphne flags. (python/djust/config.py,python/djust/mixins/post_processing.py) -
Declarative UX attributes —
dj-mutation,dj-sticky-scroll,dj-track-static(v0.6.0) — Three small client-side declarative attributes that replace boilerplatedj-hooks every production app tends to write.dj-mutation(newstatic/djust/src/37-dj-mutation.js, ~100 LOC) fires adj-mutation-fireCustomEvent when the marked element's attributes or children change via MutationObserver, withdj-mutation-attr="class,style"for targeted attribute filters anddj-mutation-debounce="N"for burst coalescing (default 150 ms).dj-sticky-scroll(new38-dj-sticky-scroll.js, ~90 LOC) keeps a scrollable container pinned to the bottom when children are appended but backs off when the user scrolls up to read history and resumes when they return to the bottom — the canonical chat / log viewer UX with a 1 px sub-pixel tolerance.dj-track-static(new39-dj-track-static.js, ~90 LOC; Phoenixphx-track-staticparity) snapshots tracked<script src>/<link href>values on page load and, on every subsequentdjust:ws-reconnectedevent, diffs against the snapshot — dispatchesdj:stale-assetsCustomEvent on changed URLs, or callswindow.location.reload()when the changed element carrieddj-track-static="reload". Without this last one, clients on long-lived WebSocket connections silently run stale JS after a deploy — zero-downtime on the server but broken behavior on connected clients. Supporting change in03-websocket.js:onopennow dispatchesdocument.dispatchEvent(new CustomEvent('djust:ws-reconnected'))on every reconnect so application code (not justdj-track-static) can hook reconnects without touching internal WS state. Convenience Django template tag{% djust_track_static %}inlive_tags.pyemits the bare attribute for discoverability. All three attributes live-register via a document-level MutationObserver root (same pattern asdj-dialog) so VDOM morphs that inject or remove the marker re-wire observers automatically. 15 JSDOM test cases acrosstests/js/dj_mutation.test.js,tests/js/dj_sticky_scroll.test.js,tests/js/dj_track_static.test.js; 4 Python test cases intests/unit/test_djust_track_static_tag.py. (python/djust/static/djust/src/37-dj-mutation.js,38-dj-sticky-scroll.js,39-dj-track-static.js,03-websocket.js,python/djust/templatetags/live_tags.py) -
djust.db.untrack(model)— disconnect signal receivers wired by@notify_on_save(#809) — Previously the only way to detach thepost_save/post_deletereceivers from a@notify_on_save-decorated model was to clear the entiresignals.receiverslist, which scorched unrelated test fixtures.untrack()now disconnects exactly the two receivers stashed onmodel._djust_notify_receiversand wipes the introspection attributes (_djust_notify_channel,_djust_notify_receivers) so a re-decoration goes through cleanly with a fresh channel. ReturnsTrueon success,Falseon a never-decorated model — idempotent, safe to call twice. Primarily for pytest teardowns in projects that decorate models at class-definition time. 5 tests intests/unit/test_db_notifications.py::TestUntrack. Exported fromdjust.dband documented in thedjust.dbmodule docstring. (python/djust/db/decorators.py,python/djust/db/__init__.py) -
Pre-minified
client.jsdistribution (v0.6.0 P1) — Production now servesclient.min.js(terser-minified) instead of the 35-module readable concat, with.gzand.brpre-compressed siblings built alongside it for whitenoise / nginx static serving. Measured impact:client.js410 KB →client.min.js146 KB raw → 39 KB gzip → 33 KB brotli (~92% reduction wire-size over the raw file).DEBUG=Truecontinues to serve the readableclient.jsso stack traces point at meaningful line numbers and contributors can poke at source directly. An explicitDJUST_CLIENT_JS_MINIFIEDsetting (bool) overrides theDEBUGheuristic in either direction so operators can validate the minified file locally or keep the readable build in production if they want to debug in-situ.scripts/build-client.shgained aminify_and_compresshelper that runs terser (fromnode_modules/.bin/terseror PATH), then gzip-9and brotli-q 11; the step is skipped gracefully when terser isn't installed so contributors can still iterate on raw sources withoutnpm install. Source-maps (.min.js.map) are emitted for production-side debugging.djust.C012system check now recognizes bothclient.jsandclient.min.jsin manual-loading detection. 6 tests intests/unit/test_client_minified.pycover build-artifact presence + size reduction, DEBUG-vs-production script selection, and the explicit override in both directions. (scripts/build-client.sh,python/djust/mixins/post_processing.py,python/djust/checks.py,package.json)
Changed
- Documented block-handler nesting + loader-access constraints (#803, #804) — Two low-priority gaps deferred from PR #802 are now surfaced in both the Rust-side
register_block_tag_handlerdocstring (crates/djust_templates/src/registry.rs) and the Python-side.pyistub (python/djust/_rust.pyi). The "no parent-tag propagation" constraint (#804) means a nested block handler is not informed it sits inside a parent handler — pass a hint throughcontextinstead. The "no loader access from handlers" constraint (#803) means block handlers cannot call{% render_template %}-style loads — pre-render child templates in the view. Both constraints were silently-true before this change; surfacing them prevents surprise when handler authors reach for features the current dispatcher doesn't yet support. No runtime behavior change. (crates/djust_templates/src/registry.rs,python/djust/_rust.pyi)
Fixed
-
assign_asyncconcurrent same-name cancellation semantics (#793) — Two rapidassign_async("metrics", loader)calls used to race: the first loader's worker thread could still be in-flight when the second call scheduled a new task, and when the slow loader finally completed, itssetattr(self, "metrics", AsyncResult.succeeded(stale))clobbered the freshAsyncResult.pending()that the second call had just written.assign_async()now maintains a per-attribute generation counter (self._assign_async_gens[name]) bumped on every call; each loader's runner closure captures the generation at creation time and short-circuits on both the success and error paths when a newer call has superseded it. The in-flight stale runner still completes (no mid-flight cancellation), but its result is discarded via a DEBUG log — the fresh pending state survives. 4 regression cases intests/unit/test_assign_async.py: sync success-path, sync error-path, async-loader success-path, and a generation-counter sanity check. (python/djust/mixins/async_work.py) -
Template dep-tracking: filter-arg bare identifiers (#787) —
{{ value|default:fallback }}now tracksfallbackas a template dependency alongsidevalue. Previously the dep-extractor walked filter chains but dropped all filter arguments, so a pattern like{% if show %}{{ value|default:dynamic }}{% endif %}would fail to re-render when onlydynamicchanged — the render cache classified the node as dep-clean and the partial-render pipeline skipped it. Literal filter args (default:"none",default:'none',default:0,default:-1) are correctly excluded from the dep set; only bare identifiers and dotted paths are tracked. Landed via a two-step:parse_filter_specsnow preserves surrounding quotes on literal args so the extractor can distinguish literals from identifiers, and render-time filter application strips quotes via the newstrip_filter_arg_quoteshelper. No change to filter runtime semantics. 15 regression cases intests/unit/test_template_dep_tracking_787_806.py. (crates/djust_templates/src/parser.rs,crates/djust_templates/src/renderer.rs) -
Template for-iterables resolve through getattr walk (#806) —
{% for x in foo.bar %}now usesContext::resolve(which walks getattr through the raw-PyObject sidecar) with a fallback toContext::get, instead of only consulting the value-stack. Previously dotted iterables silently rendered as empty when the attribute was not a top-level dict key — affecting Django QuerySet relations (user.orders), dataclass attributes, and nested Python objects. Covered by two direct-access tests (nested attributes + relation stub) + existing top-level + empty-block + missing-attr regression tests. (crates/djust_templates/src/renderer.rs) -
send_pg_notifypayload size guard (#810) — PostgreSQL capsNOTIFYpayloads at 8000 bytes.send_pg_notify()now warns at 4KB (soft limit) and drops + error-logs at 7500 bytes (hard limit). (python/djust/db/decorators.py) -
PostgresNotifyListener.areset_for_tests()awaits task cancellation (#811) — The existingreset_for_tests()fire-and-forget cancel is now documented as such; new async variant awaits the cancelled task so async test teardowns don't race. (python/djust/db/notifications.py) -
db_notifyrender-lock timeout documented (#813) — 100ms timeout is best-effort under contention; dropped notifications do not queue. (python/djust/websocket.py) -
Regression test: consumer handles views without
NotificationMixin(#812) — Locks in thatgetattr(view, '_listen_channels', None)+ truthy gate handles both absent-attr and empty-set paths. (tests/unit/test_db_notifications.py) -
stream()withlimit=Npre-trims emitted inserts (#799) — Server trimsitems_listto at-mostlimitbefore emitting inserts. (python/djust/mixins/streams.py) -
teardownVirtualListrestores original children (#798) — Teardown now restores pre-virtualization children and removes the shell/spacer. (python/djust/static/djust/src/29-virtual-list.js) -
stream_prune.childrenfilter redundancy removed (#801) — Cosmetic cleanup. (python/djust/static/djust/src/17-streaming.js) -
LiveViewTestClient.render_async()invokeshandle_async_result(#843) — Test-client drain now mirrors the production WS consumer. (python/djust/testing.py) -
LiveViewTestClient.follow_redirect()refuses to pick silently when multiple redirects queued (#844) — RaisesAssertionErrorwith all queued paths. (python/djust/testing.py) -
UploadWriter
close()return validated as JSON-serializable (#825) — Non-JSON returns caught at finalize time and abort the upload cleanly. (python/djust/uploads.py) -
BufferedUploadWriter
write_chunk()afterclose()raises (#823) —_finalizedflag now actively enforced; repeatedclose()is idempotent. (python/djust/uploads.py) -
Upload-manager drops trailing chunks silently after abort (#824, partial) — Fast-path at DEBUG log;
writer.abort()called once. (python/djust/uploads.py) -
Morph-path honors
dj-ignore-attrs(#815) — The VDOM morph loop atpython/djust/static/djust/src/12-vdom-patch.js:746-758previously stripped and overwrote attributes without consultingdjust.isIgnoredAttr. Attributes listed indj-ignore-attrswould survive individualSetAttrpatches (the guard added in PR #814) but could still get wiped during a full-element morph. The morph-path remove-loop and set-loop both now skip ignored attribute names. Two regression tests intests/js/ignore_attrs.test.jscover remove-loop and set-loop preservation. (python/djust/static/djust/src/12-vdom-patch.js)
Changed
dj-ignore-attrsCSV empty-token hardening (#816) —isIgnoredAttrnow skips empty tokens produced by double-comma ("open,,close") or trailing-comma ("open,") CSV values, and rejects empty attribute-name queries. Previously those edge cases could accidentally match an empty attribute name. Four regression tests intests/js/ignore_attrs.test.jscover empty string, whitespace-only, double comma, and trailing comma. (python/djust/static/djust/src/31-ignore-attrs.js)
Added
djust_typecheck—{% firstof %}/{% cycle %}/{% blocktrans with %}tag support (#850) — The extractor now captures positional context-variable references in{% firstof a b c %}and{% cycle a b c %}(string literals andas <name>suffixes are correctly ignored), and thewith x=expr(andcount x=expr) clauses of{% blocktrans %}/{% blocktranslate %}produce both the template-local binding (x) and the reference (expr). Eliminates a class of false positives (blocktrans locals) and false negatives (firstof/cycle args). (python/djust/management/commands/djust_typecheck.py)
Changed
-
djust_typecheck— walk MRO for parent-classself.foo = ...assigns (#851) —_extract_context_keys_from_astnow iteratescls.__mro__(skippingdjust.*,djust_*,django.*,rest_framework.*, andbuiltins), so a child view that relies on attributes set in a parentmount()no longer produces spurious "unresolved" reports. The filter drops Django'sView/ namespace-framework attrs (request,head,kwargs,args) that would otherwise surface from the base class. (python/djust/management/commands/djust_typecheck.py) -
Shared class-introspection helpers (#852) —
_walk_subclasses,_is_user_class, and_app_label_for_classare now a single source of truth in the newdjust.management._introspectmodule;djust_auditanddjust_typecheckboth import from it. No behavior change; purely a refactor to prevent drift as the set of management commands grows._introspect.walk_subclassesalso gained cycle-safety (diamond-inheritance deduplication) which the old recursive implementation lacked. (python/djust/management/_introspect.py,python/djust/management/commands/djust_audit.py,python/djust/management/commands/djust_typecheck.py) -
Service worker + main-only middleware follow-ups to PR #826 (closes #827/#828/#829/#830) —
- #828 —
DjustMainOnlyMiddlewarenow early-returns on responses withstatus_code >= 400. Error pages render full-page layouts (status message, "go back" link, etc.); trimming them to<main>would strip that context from shell-navigation clients. Regression tests cover 4xx and 5xx. - #830 — HTML response detection widened to include
application/xhtml+xmlin addition totext/html. Charset and boundary suffixes (text/html; charset=utf-8; boundary=xyz) are stripped before matching. Defensive test confirmsapplication/rss+xmlis still treated as non-HTML. - #829 —
djust.registerServiceWorker()is now idempotent. A second call returns the cached registration promise without re-runninginitInstantShell/initReconnectionBridge, so drain listeners and the WSsendMessagepatch are applied at most once. Previous behavior caused buffered replays to double on repeat init. - #827 — Documented the
<script>-inside-<main>limitation of the instant-shellinnerHTMLswap at the top of33-sw-registration.js. The doc block was also corrected:dj-click/dj-submit/etc. work through document-level event delegation (not MutationObserver), anddj-hooknow explicitly re-runs via adjust.reinitAfterDOMUpdate(placeholder)call after the swap — dj-hook content inside<main>actually works post-swap as a result (previous implementation silently skipped hook re-binding).
Tests: 9 → 13 Python cases in
tests/unit/test_main_only_middleware.py, +2 JS cases intests/js/service_worker.test.js(12 total). (python/djust/middleware.py,python/djust/static/djust/src/33-sw-registration.js) - #828 —
[0.5.1rc4] - 2026-04-22
Added
- Transport-conditional API returns —
api_response()convention +@event_handler(expose_api=True, serialize=...)override (v0.5.1 P2 follow-up to ADR-008) — Handlers serving both WebSocket and HTTP API callers often have split needs: WS only wants state mutation (VDOM renders the UI), HTTP wants actual data in the response. Serializing query results on every WS keystroke is wasteful. Resolved with three-tier resolution on the HTTP path (zero overhead on WS): (1) per-handler@event_handler(expose_api=True, serialize=<callable-or-str>)wins when set; (2) otherwise the view'sapi_response(self)method is called (the DRY convention — one method, many handlers); (3) otherwise the handler's return value passes through unchanged.serialize=accepts a callable (arity-detected:fn()/fn(view)/fn(view, result)) or a method-name string resolved against the view at dispatch time. Async serializers and asyncapi_response()are both awaited.serialize=withoutexpose_api=TrueraisesTypeErrorat decoration. Missing method or serializer exception → 500serialize_error(details logged server-side only);PermissionDeniedraised from either path surfaces as 403 (not 500). Theself._api_request = Trueflag is set by dispatch beforemount()runs so mount can branch on transport; it is retained as an escape hatch for code that needs transport awareness without the decorator plumbing. 28 tests inpython/djust/tests/test_api_response.pycover unit-level resolution (passthrough, convention, per-handler override, arity detection, async paths, MRO-provided api_response, shadowed non-callable api_response, invalid spec types, staticmethod-via-string, callable class instances) and end-to-end dispatch integration (including PermissionDenied surfacing as 403 and the mount-time flag availability). Full guide indocs/website/guides/http-api.mdunder "Transport-conditional returns". (python/djust/decorators.py,python/djust/api/dispatch.py)
[0.5.1rc3] - 2026-04-21
Added
- LiveView testing utilities (v0.5.1 P2) — Seven new methods on
LiveViewTestClientfor Phoenix LiveViewTest parity:assert_push_event(event_name, params=None)verifies a handler queued a client-bound push event (payload match is subset-based so tests stay resilient to later payload additions);assert_patch(path=None, params=None)/assert_redirect(path=None, params=None)assertlive_patch/live_redirectcalls;render_async()drains pendingstart_async/assign_asynctasks synchronously so subsequent assertions see their results;follow_redirect()resolves the queued redirect via Django's URL router and returns a new test client mounted on the destination view;assert_stream_insert(stream_name, item=None)verifies stream operations (item subset-match for dicts);trigger_info(message)synthetically delivers ahandle_infomessage so pubsub / pg_notify handlers can be tested without real backend wiring. Full user-facing guide atdocs/website/guides/testing.md. 21 new test cases. (python/djust/testing.py) dj-dialog— native<dialog>modal integration (v0.5.1 P2) — Declarative opt-in for the HTML<dialog>element's built-in modal behavior. Mark a<dialog>withdj-dialog="open"to callshowModal()(backdrop, focus-trap, and Escape handling all browser-native); setdj-dialog="close"to callclose(). A document-levelMutationObserverwatches for attribute changes and DOM insertions so VDOM morphs that swapdj-dialogwork automatically without per-element re-registration. Idempotent — re-asserting"open"on an already-open dialog is a no-op; gracefully ignores non-<dialog>elements carrying the attribute. ~80 LOC JS inpython/djust/static/djust/src/35-dj-dialog.js. 8 JSDOM tests intests/js/dj_dialog.test.js.- Type-safe template validation —
manage.py djust_typecheck(v0.5.1 P2, differentiator) — Static analysis that reads every LiveView template, extracts every variable and tag reference, and reports names not covered by the view's declared context. "Declared context" is the union of public class attributes,self.foo = ...assignments anywhere in the class (AST-extracted — not run),@propertymethods, literal-dict keys returned fromget_context_data, template-local bindings ({% for %}/{% with %}/{% inputs_for as %}), framework built-ins (user,request,csrf_token,forloop,djust, etc.), and anything listed insettings.DJUST_TEMPLATE_GLOBALS. Silencing: per-template pragma ({# djust_typecheck: noqa name1, name2 #}), per-viewstrict_context = Trueopt-in, or the project-wide globals setting. Flags:--json,--strict,--app,--view. Neither Phoenix nor React catches template-variable typos statically without an external type system — this is a genuine djust differentiator. 14 tests inpython/djust/tests/test_djust_typecheck.py. Full guide atdocs/website/guides/typecheck.md. (python/djust/management/commands/djust_typecheck.py) - Dev-mode error overlay (v0.5.1 P2) — Next.js/Vite-style full-screen error panel that renders in the browser whenever a LiveView handler raises an exception and Django
DEBUG=True. Displays the error message, the event that triggered the handler, the server-sent Python traceback, an optional hint, and validation details when present. Dismissal: Escape key, close button, or backdrop click. A second error replaces the current panel rather than stacking. All field values HTML-escaped to prevent traceback injection. Gated onwindow.DEBUG_MODE— production builds render nothing (Django also stripstraceback/debug_detail/hintfrom the error frame in non-DEBUG mode, so there's nothing to leak). Exposeswindow.djustErrorOverlay.show(detail)/.dismiss()for manual invocation from devtools. 10 JSDOM tests intests/js/error_overlay.test.js. Full guide atdocs/website/guides/error-overlay.md. (python/djust/static/djust/src/36-error-overlay.js) - Nested formset helpers —
{% inputs_for %}+FormSetHelpersMixin(v0.5.1 P2) — djust-native support for Django formset / inline-formset patterns. Template side:{% inputs_for formset as form %}...{% endinputs_for %}iterates anyBaseFormSetand exposes each bound child form with its per-row prefix intact so rendered inputs submit under the correct Django-expected names; loop metadata (inputs_for_loop.counter,.counter0,.first,.last) mirrors the{% for %}conventions. Server side:djust.formsets.add_row(cls, data=..., prefix=...)andremove_row(cls, row_prefix, data=..., prefix=...)handle management-form bookkeeping —add_rowbumpsTOTAL_FORMS(capped atmax_numwhen set,absolute_maxotherwise) and preserves existing row data;remove_rowwrites the standardDELETE=onflag soformset.deleted_formspicks it up onsave().FormSetHelpersMixinwires pre-bakedadd_row/remove_rowevent handlers to aformset_classes = {"addresses": AddressFormSet}declaration, with the formset name doubling as the prefix so multiple formsets on one view don't collide on management-form keys. Fails loud ifmount()forgets to initializeself._formset_data. 16 tests inpython/djust/tests/test_formsets.py. (python/djust/formsets.py,python/djust/templatetags/djust_formsets.py)
[0.5.1rc2] - 2026-04-21
Added
- Scaffold CSS — reusable layout/utility pack in
djust.theming—djust_theming/static/djust_theming/css/scaffold.cssgains ~729 lines of framework-generic scaffold covering typography, responsive grid utilities (.grid-2/3/4), hero section, flash messages (Django + LiveView), accessibility utilities (.sr-only), extended layout helpers (.flex-center,.content-narrow/-wide), stat-display variants, auth layout, live indicator dot, card-accent variants, code blocks, noise texture overlay, shared nav links, dashboard/centered grids, and the fulldata-layoutswitching system (sidebar, topbar, dashboard, centered, sidebar-topbar). All new rules use CSS-variable fallbacks so the scaffold works without a loaded theme; no hardcoded hex colors;.containermax-width now readsvar(--container-width, 1200px). Pure-CSS addition — no Python/JS/test behavior changes. (PR #836)
Fixed
- All 82 pre-existing test failures resolved (PR #841) — The
make testbaseline went from2135 passed, 61 failed, 21 errors(which had blocked normal merges for the entire v0.5.1 milestone and forced--adminon every PR) to2219 passed, 0 failed, 0 errors. Four fix clusters:- Test-infrastructure shims (64 fixes) — added
tests/gallery_test_urls.pyandtests/test_critical_css.pyURL-conf shims that theming tests reference via@override_settings(ROOT_URLCONF=...)but were never created; addedmcp[cli]>=1.2.0; python_version >= '3.10'to dev deps sodjust.mcpserver tests stop throwingModuleNotFoundError. - Stale
@layertest expectations (4 fixes) — several theming CSS files (components.css,layouts.css,pages.css, critical-CSS generator) were intentionally unwrapped from@layerblocks for specificity reasons (documented in file headers); updated tests to match the current design using@layer NAME {block-syntax regex rather than substring match. - Real code bugs (3 fixes) —
ocean_deeppreset's internalnamewas"ocean"while its registry key was"ocean_deep"; one straytext-align: leftincomponents.css.tp-select-optionbroke RTL support (changed totext-align: start); and the CSS prefix generator's hand-maintained_COMPONENT_CLASSESlist had drifted fromcomponents.css—.btn-edit,.btn-remove,.avatar,.breadcrumb,.dropdownand many more weren't being prefixed when a customcss_prefixwas set. Replaced with auto-extraction via regex over the static file; stays in sync automatically. - Stale test assumption (1 fix) —
test_list_same_content_no_renderencoded a wrong assumption about_snapshot_assigns(identity-based by design); rewrote to match the documented contract.
- Test-infrastructure shims (64 fixes) — added
- CSS prefix generator hardening — Auto-extraction regex gained a negative lookbehind
(?<![\w])to prevent capturing domain fragments inside data-URIs (previously.organd.w3inhttp://www.w3.org/2000/svgwere mis-captured as class selectors, producinghttp://www.dj-w3.dj-org/2000/svgunder prefix); compound state-class chains like.wizard-step.completednow correctly leave the trailing state class unprefixed (JS toggles state classes by bare name, so they must NOT get the prefix). Two new regression tests (test_data_uri_domains_are_not_mis_prefixed,test_compound_state_classes_stay_unprefixed) lock both in.
Changed
- ROADMAP.md audit correction — Five entries marked as "v0.5.1 Not started" were actually shipped earlier: djust-theming fold (v0.5.0 PR #772), WizardMixin (PR #632), Error boundaries (v0.5.0 PR #773), and
dj-lazylazy LiveView hydration (PR #54). All marked with strikethrough + ✅ and a shipped-in PR pointer. Real v0.5.1 remainder after audit: LiveView testing utilities, type-safe template validation, error overlay,inputs_fornested formsets, native<dialog>(5 items instead of 8).
[0.5.1rc1] - 2026-04-21
Added
-
Form & submit polish batch (v0.5.1 P2) — Three related form-UX primitives:
dj-no-submit="enter"— Prevent Enter-key form submission from text inputs. Fixes the #1 form UX annoyance where pressing Enter to confirm a field accidentally submits the whole form. Textareas (multi-line input), submit-button clicks, and modified keys (Shift+Enter, Ctrl+Enter) are unaffected. Supports comma-separated modes (currently only"enter") for future expansion. Document-level keydown listener — DOM morphs don't need re-registration. (python/djust/static/djust/src/34-form-polish.js)dj-trigger-action+self.trigger_submit(selector)— Bridge successful djust validation to a native HTML form POST. Essential for OAuth redirects, payment gateway handoffs, and anywhere the final step needs a real browser POST. The server callsself.trigger_submit("#form-id")after validation passes; the client receives the push event, verifies the target form carriesdj-trigger-action(explicit opt-in — refusal is logged in debug mode), and calls the form's native.submit(). (python/djust/mixins/push_events.py,python/djust/static/djust/src/34-form-polish.js)dj-loading="event_name"shorthand — Declarative scoped loading indicator:<div dj-loading="search">Searching...</div>shows only while thesearchevent is in-flight. Previously required combiningdj-loading.show+dj-loading.for="event_name"with an inlinestyle="display:none". The shorthand auto-hides the element on register (no inline style required) and treats the attribute value as both the event-scope and the implicit.showtrigger. Coexists with the existingdj-loading.*modifier family. (python/djust/static/djust/src/10-loading-states.js)
Tests: 11 JS test cases in
tests/js/form_polish.test.jscovering every happy path and failure mode; 4 Python tests inpython/djust/tests/test_trigger_submit.pylocking in the push-event shape. Client.js: 35 → 36 source modules (+~120 LOC JS, +~30 LOC Python). Scoped scoped-loadingdj-loading="event"implementation reuses existingglobalLoadingManagerinfrastructure — no duplication. -
State & computation primitives batch (v0.5.1 P2) — Four small related primitives for derived state, dirty tracking, stable IDs, and cross-component context sharing:
- Memoized
@computed("dep1", "dep2")—@computednow accepts an optional tuple of dependency attribute names. When given, the value is cached on the instance and only recomputed when any dep's identity or shallow content fingerprint changes (id + length + key subset matching_snapshot_assignssemantics). Plain@computed(no args) retains property semantics — recomputes every access. ReactuseMemoequivalent. (python/djust/decorators.py) - Automatic dirty tracking —
self.is_dirty/self.changed_fields/self.mark_clean()— Track which public view attributes have changed since a baseline captured aftermount().changed_fieldsreturns a set of attr names that differ from the baseline;is_dirtyisbool(changed_fields);mark_clean()resets the baseline (call after a successful save). Use cases: "unsaved changes" warnings (beforeunload), conditional save buttons, optimizedhandle_eventthat skips work when nothing changed. Respectsstatic_assignsand ignores private attrs. The WebSocket consumer and the HTTP API dispatch view both capture the baseline after mount. (python/djust/live_view.py,python/djust/websocket.py,python/djust/api/dispatch.py) - Stable
self.unique_id(suffix="")— React 19useIdequivalent. Returns a deterministic per-view ID stable across renders of the same logical position. Useful foraria-labelledby, form field IDs, and any element that needs a consistent identifier across re-renders. Format:djust-<viewslug>-<n>[-<suffix>]. Counter resets viareset_unique_ids()at render boundaries. (python/djust/live_view.py) - Component context sharing —
self.provide_context(key, value)/self.consume_context(key, default=None)— React Context API equivalent. A parent view or component exposes a value underkey; descendants look it up withconsume_context, walking the_djust_context_parentchain. Scoped per render tree;clear_context_providers()resets. (python/djust/live_view.py)
- Memoized
-
Auto-generated HTTP API from
@event_handler(v0.5.1 P1 HEADLINE, ADR-008) — Opt-in@event_handler(expose_api=True)exposes a handler atPOST /djust/api/<view_slug>/<handler_name>/with an auto-generated OpenAPI 3.1 schema served at/djust/api/openapi.json. Unlocks non-browser callers (mobile, S2S, CLI, AI agents) without duplicating business logic — the HTTP transport is a thin adapter over the existing handler pipeline, reusingvalidate_handler_params(),check_view_auth(),check_handler_permission(), and the same_snapshot_assigns()/_compute_changed_keys()diff machinery the WebSocket path uses. One stack, one truth (manifesto #4). New packagedjust.apiwithDjustAPIDispatchView(dispatch view),api_patterns()(URL factory),OpenAPISchemaView(schema endpoint),SessionAuth+ pluggableBaseAuthprotocol (auth classes may opt out of CSRF viacsrf_exempt = True), and a registry that walksLiveViewsubclasses with exposed handlers.LiveViewgains two read-only contract attributes:api_name(stable URL slug) andapi_auth_classes(auth class list). Response shape mirrors the WS assigns-diff:{"result": <return>, "assigns": {<changed public attrs>}}. Error shapes are structured witherror/message/details— 400 validation, 401 unauth, 403 denied or CSRF fail, 404 unknown view/handler or handler notexpose_api=True, 429 rate limit, 500 handler exception (exception messages logged server-side only, never leaked to the client). Rate limiting: HTTP uses a process-level LRU-capped token bucket keyed on(caller, handler_name)honoring the handler's@rate_limitsettings; WebSocket continues to use its per-connectionConnectionRateLimiter. The two transports share rate/burst values but separate bucket storage — a caller using both draws from both independently (a shared-bucket refactor is tracked as a follow-up).manage.py djust_auditnow lists everyexpose_api=Truehandler and flags any missing@permission_required— treat an exposed handler like@csrf_exempt. Out of scope per ADR-008: streaming responses, GraphQL batching, first-party token auth, Swagger UI hosting, per-handler URL customization. Full guide atdocs/website/guides/http-api.md. (python/djust/api/,python/djust/decorators.py,python/djust/live_view.py,python/djust/management/commands/djust_audit.py) -
Service worker core improvements — instant page shell + WebSocket reconnection bridge (v0.5.0 P3, opt-in) — Two independent SW features that close the v0.5.0 milestone. Both are OFF by default; users opt in explicitly via
djust.registerServiceWorker({ instantShell: true, reconnectionBridge: true })from their own init code. No auto-registration.- Instant page shell. The SW caches the first navigation's response split into a "shell" (everything outside
<main>) and "main" (inside). Subsequent navigations serve the cached shell immediately with a<main data-djust-shell-placeholder="1">placeholder; the client then fetches the current URL withX-Djust-Main-Only: 1and swaps in the fresh<main>contents. Shell/main split uses a single non-greedy regex — nested<main>inside HTML comments or</main>insideCDATAare documented limitations (full HTML parser deferred). Server side honors the header via the newdjust.middleware.DjustMainOnlyMiddleware, which extracts the first<main>…</main>inner HTML, updatesContent-Length, and stampsX-Djust-Main-Only-Response: 1. The middleware only touches HTML responses; JSON / binary / streaming responses pass through unchanged. Ordering-safe — it can sit anywhere inMIDDLEWAREthat sees the rendered response. - WebSocket reconnection bridge. Client-side wraps
LiveViewWebSocket.sendMessageso that whenws.readyState !== OPENthe serialized payload is posted to the SW viapostMessage({type: 'DJUST_BUFFER', connectionId, payload})instead of being dropped. The SW stores messages in an in-memoryMapkeyed by connection id, capped at 50 per connection (oldest dropped). On reconnect the client firesDJUST_DRAIN; the SW returns the buffered payloads and the client replays each viaws.ws.send(). Per-page-load connection ids isolate buffers across tabs. IndexedDB persistence and server-side sequence-dedup replay are deferred to v0.6 (best-effort replay today). - Files:
python/djust/static/djust/service-worker.js(new, standalone — NOT bundled intoclient.js),python/djust/static/djust/src/33-sw-registration.js(new, concatenated intoclient.js),python/djust/middleware.py(new),python/djust/config.py(newservice_workerdefaults sub-dict), tests intests/js/service_worker.test.js(10 cases) andtests/unit/test_main_only_middleware.py(7 cases), full guide atdocs/website/guides/service-worker.md.
- Instant page shell. The SW caches the first navigation's response split into a "shell" (everything outside
-
UploadWriter— raw upload byte-stream access for direct-to-S3 / GCS streaming (Phoenix 1.0 parity, v0.5.0 P2) — NewUploadWriterbase class indjust.uploadswith anopen()→write_chunk(bytes)→close() -> Any/abort(error)lifecycle, wired intoallow_upload(name, writer=MyWriter). When a writer is configured, binary WebSocket chunks are piped straight to the writer without buffering to disk or RAM — zero temp file, zeroentry._chunks. Writers are instantiated lazily per upload on the first chunk (so abandoned uploads never open an S3 multipart upload), opened exactly once, fedwrite_chunk()per client frame, and finalized viaclose()whose return value is stored onUploadEntry.writer_resultand rendered in the upload-state context as{{ entry.writer_result }}. Any failure (open or write_chunk raised,close()raised, size-limit exceeded, client cancelled, WebSocket disconnected viaUploadManager.cleanup()) routes throughabort(BaseException)with the raw exception so writers can release server-side resources (e.g.AbortMultipartUpload);abort()is wrapped to swallow its own exceptions so a failing S3 cleanup never propagates into the request path. IncludesBufferedUploadWriterhelper that accumulates client-sent 64 KB chunks until a configurablebuffer_threshold(default 5 MB — S3 MPU minimum part size except for the last) and callson_part(bytes, part_num)so subclasses work with S3-aligned parts without worrying about raw client chunk size. Legacy (no-writer=) disk-buffered path is untouched byte-for-byte — backward compatible. Documented indocs/website/guides/uploads.mdwith a full S3 multipart example. (python/djust/uploads.py,python/djust/websocket.py) -
dj-ignore-attrs— per-element client-owned attribute opt-out (Phoenix 1.1JS.ignore_attributes/1parity, v0.5.0 P2) — Mark specific HTML attributes as client-owned so VDOMSetAttrpatches skip them.<dialog dj-ignore-attrs="open">prevents the server from resetting theopenattribute that the browser manages;<div dj-ignore-attrs="data-lib-state, aria-expanded">protects third-party JS state. Comma-separated list with whitespace tolerance. The guard sits insideapplySinglePatch'scase 'SetAttr'after theUNSAFE_KEYScheck; the attribute write is skipped entirely (andbreaks out of the case) when the element opts out.RemoveAttris intentionally unaffected. Implementation:globalThis.djust.isIgnoredAttr(el, key)helper (~20 lines JS) plus a three-line check in the patch site. (python/djust/static/djust/src/31-ignore-attrs.js,python/djust/static/djust/src/12-vdom-patch.js) -
{% colocated_hook %}template tag + runtime extraction (Phoenix 1.1ColocatedHookparity, v0.5.0 P2) — Write hook JavaScript inline alongside the template that uses it, instead of in a separate file.{% colocated_hook "Chart" %}hook.mounted = function() { renderChart(this.el); };{% endcolocated_hook %}emits a<script type="djust/hook" data-hook="Chart">tag with a/* COLOCATED HOOK: Chart */auditor banner. The client runtime walksscript[type="djust/hook"]elements on init and after each VDOM morph (reinitAfterDOMUpdate), registers each body aswindow.djust.hooks[name]vianew Function, and marks the script withdata-djust-hook-registered="1"so re-scans are idempotent. Optional namespacing viaDJUST_CONFIG = {"hook_namespacing": "strict"}prefixesdata-hookwith<view_module>.<view_qualname>so two views can each defineChartwithout colliding; per-tag opt-out with{% colocated_hook "X" global %}. Namespacing is OFF by default for compat. Security: the body is template-author JS (same trust level as any other template JS);</script>/</SCRIPT>are escaped in the tag'srender()to prevent premature tag close. Apps on strict CSP without'unsafe-eval'should continue using the traditional registration pattern. (python/djust/static/djust/src/32-colocated-hooks.js,python/djust/templatetags/live_tags.py,python/djust/config.py,docs/website/guides/hooks.md) -
Database change notifications — PostgreSQL
LISTEN/NOTIFY→ LiveView push (v0.5.0 P1) — Subscribe LiveViews to Postgres pg_notify channels so database changes push real-time updates to every connected user with zero explicit pub/sub wiring. Three APIs:@notify_on_save(channel="orders")model decorator hooks Djangopost_save/post_deleteand emitsNOTIFY <channel>, <json>;self.listen("orders")inmount()subscribes the view (joins a Channels group nameddjust_db_notify_<channel>);def handle_info(self, message)receives{"type": "db_notify", "channel": ..., "payload": {"pk": ..., "event": "save"|"delete", "model": "app.Model"}}and re-renders via the standard VDOM diff path. A process-widePostgresNotifyListenerowns one dedicatedpsycopg.AsyncConnection(outside Django's pool — long-lived LISTEN connections don't play nice with pgbouncer transaction pooling) and runsasync for notify in conn.notifies():, bridging every NOTIFY intochannel_layer.group_send(...). Channel names are strictly validated (^[a-z_][a-z0-9_]{0,62}$) at registration and listen time — load-bearing because Postgres NOTIFY doesn't accept bind parameters for the channel identifier.send_pg_notify(channel, payload)is a public helper for Celery tasks / management commands. Non-postgres backends no-op gracefully (debug-logged);self.listen()raisesDatabaseNotificationNotSupportedwhen psycopg or a postgres backend isn't available. Known limitation: notifications emitted while the listener's TCP connection is dropped are lost — listener auto-reconnects with 1s backoff and re-issues LISTEN for all subscribed channels, and WSmount()re-fetch handles the client-side recovery case. Documented indocs/website/guides/database-notifications.md. (python/djust/db/decorators.py,python/djust/db/notifications.py,python/djust/mixins/notifications.py,python/djust/websocket.py) -
PyO3
getattrfallback for model attribute access (v0.5.0 P1 — Rust template engine parity) — Templates can now reference Django model instances passed through context without manual dict conversion.{{ user.username }}resolves via Pythongetattrwhenuseris a raw Python object rather than a JSON-serialized dict. Implementation: Python's_sync_state_to_rust()builds a sidecar of non-JSON-friendly context values and forwards them via the newRustLiveView.set_raw_py_values()method; Rust'sContext::resolve()tries the normal value-stack path first, then walksgetattron attached PyObjects one segment at a time.PyAttributeError(and any property-descriptor exceptions) are caught — missing attrs render as empty, matching Django'sTEMPLATE_STRING_IF_INVALIDdefault.ValuestaysSerialize-friendly (noValue::PyObjectvariant); sidecar lives outside the Value enum viaArc<HashMap<String, PyObject>>onContext. (crates/djust_core/src/context.rs,crates/djust_live/src/lib.rs,python/djust/mixins/rust_bridge.py) -
register_assign_tag_handler()for context-mutating template tags (v0.5.0 P1 — Rust template engine parity) — New tag-handler variety complementingregister_tag_handler(emits HTML) andregister_block_tag_handler(wraps content). An assign tag'srender(args, context)method returns adict[str, Any]that's merged into the template context for subsequent sibling nodes — no HTML output. Enables{% assign slot var_name %}-style patterns. Supported inside{% for %}loops (per-iteration mutation). Registered viadjust._rust.register_assign_tag_handler(name, handler). NewNode::AssignTagvariant; partial-renderer emits"*"wildcard dep so downstream nodes always re-render on context changes. (crates/djust_templates/src/registry.rs,crates/djust_templates/src/parser.rs,crates/djust_templates/src/renderer.rs) -
dj-virtual— Virtual / windowed lists with DOM recycling (v0.5.0 P1) — Render only the visible slice of a large list, recycling DOM nodes as the user scrolls.<div dj-virtual="items" dj-virtual-item-height="48" dj-virtual-overscan="5" style="height: 600px; overflow: auto;">keeps ~visible-plus-overscan children in the DOM even if the pool has 100K entries. Implementation: fixed-height windowing viatransform: translateY(...)on an inner shell plus a hidden spacer for scrollbar length, scroll handler batched throughrequestAnimationFrame, real element identity preserved across scrolls for hook/framework compatibility. Integrates with the VDOM morph pipeline: new containers are picked up byreinitAfterDOMUpdate, anddjust.refreshVirtualList(el)is available for explicit repaints.djust.teardownVirtualList(el)disconnects observers for unmounted containers. (python/djust/static/djust/src/29-virtual-list.js) -
dj-viewport-top/dj-viewport-bottom— Bidirectional infinite scroll (Phoenix 1.0 parity, v0.5.0 P1) — Fire server events when the first or last child of a stream container enters the viewport viaIntersectionObserver.<div dj-stream="messages" dj-viewport-top="load_older" dj-viewport-bottom="load_newer" dj-viewport-threshold="0.1">. Once-per-entry firing (matches Phoenix) via adata-dj-viewport-firedsentinel; calldjust.resetViewport(container)or replace the sentinel child to re-arm. New server-sidestream()limit=Nkwarg andstream_prune(name, limit, edge)method emit astream_pruneop that trims children from the opposite edge so chat apps, activity feeds and log viewers can stream bidirectionally without unbounded DOM growth. (python/djust/static/djust/src/30-infinite-scroll.js,python/djust/static/djust/src/17-streaming.js,python/djust/mixins/streams.py) -
assign_async/AsyncResult(v0.5.0 P1) — High-level async data loading inspired by Phoenix LiveView'sassign_async. Callself.assign_async("metrics", self._load_metrics)inmount()(or any event handler); the attribute is set toAsyncResult.pending()immediately, the loader runs via the existingstart_asyncinfrastructure, and on completion the attribute becomesAsyncResult.succeeded(result)orAsyncResult.errored(exc). Templates read the three mutually-exclusive states via{% if metrics.loading %}…,{% if metrics.ok %}{{ metrics.result }}…,{% if metrics.failed %}{{ metrics.error }}…. Sync andasync defloaders are both supported; multiple calls in the same handler load concurrently. Cancellation piggybacks oncancel_async("assign_async:<name>"). (python/djust/async_result.py,python/djust/mixins/async_work.py) -
{% dj_suspense %}block tag for template-level loading boundaries (v0.5.0 P1) — Declarative counterpart toassign_async: wrap a section depending on one or moreAsyncResultassigns, and the boundary emits a fallback while any are loading, an error div if any failed, or the body once all areok. Explicitawait="metrics,chart"syntax keeps the tag debuggable — no reflection magic. Fallback templates are loaded via Django's template loader; unspecified fallbacks render a minimal spinner. Nested suspense boundaries resolve independently. Registered alongside{% call %}in the Rust template engine — no parser/renderer changes. (python/djust/components/suspense.py,python/djust/components/rust_handlers.py) -
Function components via
@componentdecorator (v0.5.0 P1 batch) — Stateless Python render functions registerable as template-invokable components.@component def button(assigns): ...is callable from templates via{% call "button" variant="primary" %}Go{% endcall %}(with{% component %}as a synonymous alias). Closes the middle ground between raw HTML and fullLiveComponentclasses for the ~80% of UI pieces (buttons, cards, badges, icons) that are stateless.clear_components()helper exposed for tests. (python/djust/components/function_component.py,python/djust/__init__.py) -
Declarative component assigns and slots (Phoenix.Component parity) —
Assign("variant", type=str, default="default", values=["primary", "danger"], required=True)andSlot("col", multiple=True)DSL, declared on aLiveComponentclass attribute (assigns = [...]/slots = [...]) or on function components via@component(assigns=[...], slots=[...]). Validation runs at mount/invoke: required-missing raisesAssignValidationErrorin DEBUG and warns in production, type coercion (str → int / bool / float) is automatic, enum violations viavalues=raise. Child-classassignsextend (and override by name) parent declarations via MRO walk. (python/djust/components/assigns.py,python/djust/components/base.py) -
Named slots with attributes via
{% slot %}/{% render_slot %}tags — Parent templates pass named content blocks with attributes into components:{% call "card" %}{% slot header label="Title" %}Header{% endslot %}Body{% endcall %}. Multiple same-name slots collect into a list (essential for table columns, tab panels). Slots are exposed to the component asassigns["slots"] = {name: [{"attrs": {...}, "content": "..."}, ...]}. Non-slot content in the{% call %}body becomeschildren/inner_block. Implemented in pure Python via a sentinel-and-extract protocol — zero Rust parser/renderer changes. (python/djust/components/function_component.py)
Fixed
- Attribute-context HTML escaping parity with Django (v0.5.0 P1 — Rust template engine parity) — Variables inside HTML attribute values now route through a dedicated
html_escape_attr()that's guaranteed to escape"→"and'→'(in addition to&/</>). Detection reuses the existingis_inside_html_tag_at()parser helper — the per-Node::Variablein_attrflag is computed at parse time, so renderer cost is a bool check.|safestill bypasses escaping in both attribute and text contexts. Today's behaviour is unchanged (the basehtml_escapealready covered quotes) — this refactor makes the parse-time classification visible to the renderer so future changes to the default escape can't accidentally break attribute values like<a href="{{ url }}">whenurlcontains quotes. (crates/djust_templates/src/parser.rs,crates/djust_templates/src/filters.rs,crates/djust_templates/src/renderer.rs) - Inline conditional
{{ x if cond else y }}now contributes deps to enclosing wrappers (#783, sibling bug) — Same failure mode as nested{% include %}:extract_from_nodeshad no arm forNode::InlineIf, so itstrue_expr/condition/false_exprvariables were silently dropped from the dep set of any surrounding{% if %}/{% for %}/{% with %}. Changing the condition alone (e.g.step_activein{% for s in steps %}<span class="{{ 'active' if step_active else 'idle' }}">) producedpatches=[]and stale HTML. Fix:extract_from_nodesnow extracts non-literal variables from all threeInlineIfexpressions. - Nested
{% include %}now propagates wildcard dep to enclosing wrappers (#783) — Rust partial renderer reused the cached fragment of an{% if %}/{% for %}/{% with %}wrapping a nested{% include %}, becauseextract_from_nodestreatedIncludeas having no variable references. When the included template referenced a context key that changed (e.g.{{ field_html.first_name|safe }}), the wrapper's dep set ({current_step_name}) did not intersectchanged_keys({field_html}),needs_renderreturnedfalse, the cached HTML was reused, and the text-region fast-path compared byte-identical old/new HTML →patches=[]withdiff_ms: 0. Manifested with deeply-nestedWizardMixintemplates ({% extends %} → {% block %} → {% if current_step_name == "..." %} → {% include "step_*.html" %}). Fix:extract_from_nodesnow injects"*"into the variables map when it encounters a nestedIncludeorCustomTag/BlockCustomTagduring its walk, so wrapper deps include the wildcard and those nodes are always re-rendered. (crates/djust_templates/src/parser.rs) _force_full_htmlnow callsset_changed_keysso Rust partial renderer re-renders (#783) — When_force_full_htmlwas set,_sync_state_to_rust()clearedprev_refsto force all context to Rust, but theset_changed_keyscall was gated byif prev_refswhich evaluated to False after clearing. Rust's partial renderer saw nochanged_keys, fell back to full render with emptychanged_indices, and the text-region fast-path compared identical old/new HTML → zero patches. Fix:set_changed_keysis now called when_force_full_htmlis set regardless ofprev_refs.
Docs
- ROADMAP correction:
temporary_assignsis already implemented — The v0.5.0 ROADMAP entry claimingtemporary_assignswas "completely absent from djust today" was inaccurate. The feature has shipped in earlier releases (LiveView._initialize_temporary_assigns/_reset_temporary_assigns, wired into the render cycle and excluded from change tracking). This PR adds a dedicated regression test (tests/unit/test_temporary_assigns.py) — prior coverage was indirect — and strikes through the ROADMAP entry.
Tests
- Regression coverage for
temporary_assigns—tests/unit/test_temporary_assigns.pycovers reset-after-render semantics, default-value cloning per type (list / dict / set / scalar), idempotent initialization, pre-existing-attribute preservation, instance-level override, and the empty-mapping no-op path. - Unit tests for
assign_async/AsyncResult—tests/unit/test_assign_async.py(18 tests) covers state-flag invariants, frozen dataclass immutability, pending-is-set-immediately, success & failure propagation, multi-concurrent scheduling, cancellation interop withcancel_async, sync and async loaders, args/kwargs forwarding, and the generation-counter / stale-loader regression cases added in #793. - Unit tests for
{% dj_suspense %}—tests/unit/test_suspense.py(12 tests) covers ok → body, loading → fallback, failed → error-div, HTML-escaped error messages, no-await=passthrough, unknown / non-AsyncResultrefs defaulting to loading, default spinner, Django template fallback, template-error graceful degradation, nesting, and whitespace-tolerant comma-separated lists. - Regression suite for
|safeHTML blob diff (#783) —tests/test_rust_vdom_safe_diff_783.pyexercises the WizardMixin-style pattern wherefield_htmlis derived inget_context_data()from an instance attribute. Covers dict reassignment, in-place nested mutation, the_force_full_htmlcodepath, an{% if %}branch swap, a{% extends %}/{% block %}inheritance chain, and the exact NYC-Claims-style{% extends %} + {% if %} + {% include %}structure that originally exhibited the bug. All variants assert non-empty VDOM patches on state change. - Dep-extractor hardening (#783 follow-up, P0) — Three-part hardening against silent dep-drop regressions in
crates/djust_templates/src/parser.rs::extract_from_nodes:- Rust unit tests for
extract_per_node_deps— table-driven assertions on representative AST shapes (simple Variable, If-wrapping-Include, For with tuple unpacking, With + body, InlineIf condition, nested For, Block recursion, plain Text). Explicit"*"wildcard membership checks for nestedInclude/CustomTagshapes. - Node variant exhaustiveness check —
sample_for_coverageexhaustive match onNode::*+sample_nodes()constructor +NO_VARS_VARIANTSallow-list. Any newNodevariant fails to compile until the match is updated, and at runtime every non-allow-listed variant must produce a non-empty dep set (real vars or"*"wildcard). Makes silent dep-drops on future additions impossible. - Partial-render correctness harness (Python) —
TestPartialRenderCorrectnessintests/test_rust_vdom_safe_diff_783.py. Byte-equality oracle: for each of 6 wrapper shapes (no-wrapper,{% if %},{% for %},{% with %}, full #783 extends/if/include/safe chain, InlineIf-in-for), renders a mutation via the normal partial-render path then re-renders the same mutation with the Rust fragment cache cleared (clear_fragment_cache()) as a control. Any dep-miss that causes partial render to reuse a stale cached fragment diverges from the control and fails.
- Rust unit tests for
- New PyO3 method
DjustLiveView.clear_fragment_cache(test-only) (crates/djust_live/src/lib.rs) — clearsnode_html_cache,last_html,fragment_text_map,text_node_indexwhile preservinglast_vdomso the diff baseline is unchanged. Exclusively supports the partial-render correctness harness above; not intended for application use.
[0.5.0rc2] - 2026-04-20
Added
- Bootstrap 4 CSS framework adapter — New
Bootstrap4Adapterfor projects using Bootstrap 4 (NYC Core Framework, government sites, legacy projects). SetDJUST_CONFIG = {"css_framework": "bootstrap4"}. Includes propercustom-select,custom-control-*classes for checkboxes/radios, andform-groupwrappers. - Dedicated radio button classes — Radio buttons now use
radio_class,radio_label_class, andradio_wrapper_classconfig keys (with fallback to checkbox classes). Both Bootstrap 4 and 5 configs define radio-specific classes. - Select widget class support —
ChoiceFieldwithSelectwidget usesselect_classconfig key (e.g.,custom-selectfor BS4,form-selectfor BS5) instead of the genericfield_class. - Theme-to-framework CSS bridge — New
{% theme_framework_overrides %}template tag generates<style>overrides that map djust theme variables (--primary,--border, etc.) onto the active CSS framework's selectors (.btn-primary,.form-control,.alert-*, etc.). Switching themes now automatically re-styles Bootstrap 4/5 components.
Fixed
- Derived container context values now tracked by value equality (#774) — The Rust state sync used
id()comparison for all non-immutable context values, which is unreliable for containers (dict, list, tuple) due to CPython address reuse after GC. Derived values likecurrent_step = wizard_steps[step_index]could be missed when the handler only changedstep_index, causing Rust to render stale HTML. Fix: containers are now compared by value equality (like immutables already were), with previous values cached in_prev_context_containers. The optimization is preserved — unchanged containers are still skipped.
[0.5.0rc1] - 2026-04-19
Added
- Package consolidation: all 5 runtime packages folded into djust — One install, one version, one CHANGELOG.
pip install djuststays lean;pip install djust[all]gets everything.- Phase 1+2:
djust-auth+djust-tenants→ core (#770) —djust-auth(879 LOC) merged intopython/djust/auth/package with lazy imports.djust-tenantsmissing modules (audit, middleware, managers, models, security) merged into existingpython/djust/tenants/. Both are core — no extras needed. 27 new tests. - Phase 3:
djust-admin→djust[admin](#771) — 3,878 LOC merged intopython/djust/admin_ext/(avoids collision withdjango.contrib.admin). Views, forms, adapters, plugins, decorators, template tags, 7 HTML templates. 40 new tests. - Phase 4:
djust-theming→djust[theming](#772) — 49,105 LOC merged intopython/djust/theming/. CSS theming engine, design tokens, 96 HTML templates, 9 static files, management command (djust_theme), 4 template tag modules, gallery sub-package. 749+ tests. - Phase 5:
djust-components→djust[components](#773) — ~100K LOC merged intopython/djust/components/. 170+ UI component classes, 6 template tag modules, management command (component_gallery), descriptors, mixins, rust_handlers.py. Extra deps:markdown>=3.0,nh3>=0.2.
- Phase 1+2:
[0.4.5rc2] - 2026-04-18
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+.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).seed_fixtures(fixture_paths)in djust MCP — Subprocess wrapper aroundmanage.py loaddatafor regression-fixture DB setup.
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.
[0.4.5rc1] - 2026-04-17
Changed
-
Text-region fast path now fires for
{% extends %}templates — The scanner that builds the VDOM text-node position index used to process the full pre-hydration HTML, but the VDOM is rooted at[dj-root]. On templates extending a base (with<title>, meta tags, scripts outside dj-root), the scanner counted text runs in<head>and trailing<footer>/<script>siblings that the VDOM didn't have — the count mismatched, the index was discarded, and every event fell through to a full html5ever parse (~10ms on the djust.org /examples/ page). Now the scanner is restricted to the dj-root element's interior via a balanced-tag walker. Rust render drops from ~14ms → ~2.8ms on extends templates; browser E2E (production, DEBUG=False) drops from 30ms → ~25ms avg, 18ms min. -
Text-region VDOM fast path — Extends the existing text-fast-path to handle changes that differ only in a text span, even when the surrounding fragment contains tags. Computes byte-level common prefix/suffix on pre-hydration HTML; if the divergence is a single tag-free text run, locates the owning VDOM text node via a pre-built positional index (binary search on
(html_start, html_end, path, text, djust_id)entries, built once per full-parse render and kept in sync through fast-path events by shifting downstream entries by the byte delta). Patches in place and skips html5ever entirely. For a counter click inside a{% for %}loop on a 309KB page, Rust render drops from ~12ms to ~2.7ms. UTF-8 safe (snaps to char boundaries), handles<pre>/<code>/<textarea>whitespace preservation and<script>/<style>raw-text element bodies correctly, bails to full parse on entity-offset mismatches. -
parse_html_fragment(html, context_tag)— New public entry point indjust_vdomthat uses html5ever'sparse_fragmentwith a parent-element context. Enables parsing isolated HTML fragments with correct tokenization for context-sensitive elements (<tr>,<td>,<option>), without resetting the dj-id counter. Scaffolding for future structural-fragment fast paths. -
collect_vdom_text_nodesnow skips comment nodes — Previously collected<!--dj-if-->placeholders into the text-node list, shifting every subsequent ordinal by one and breaking any position-based patching. Text and comment VNodes both carrytext, so an explicitis_text()filter was needed. -
Partial template rendering (#737) — Per-node dependency tracking at template parse time. On re-render, only template nodes whose context variable dependencies changed are re-rendered; unchanged nodes reuse cached HTML. For a single-variable change on a page with 50 template nodes, template render drops from ~1.4ms to ~0.1ms. Changed keys are passed from Python to Rust via
set_changed_keys(), which merges across multiple sync calls.{% include %}and custom tags always re-render (wildcard dependency). -
{% extends %}inheritance resolution caching — Templates using{% extends %}now participate in partial rendering. Inheritance is resolved once viaOnceLock<ResolvedInheritance>on theTemplatestruct (shared viaTEMPLATE_CACHE). Final merged nodes and their deps are cached, so subsequent renders skip both chain building and static parent nodes. Combined with partial rendering, extends templates go from full re-render (~14ms Rust) to partial render of changed nodes only (~0.02ms Rust). -
Text-only VDOM fast path — When all changed template fragments are plain text (no HTML tags), skip both html5ever parsing and VDOM diffing entirely. The old VDOM is mutated in-place via a fragment→text-node map built on first render, and SetText patches are produced directly. For counter-style updates: parse phase drops from ~12ms to ~0.001ms.
-
Block flattening for partial rendering —
{% block %}nodes left by Django's template engine are flattened to expose each child as a separate fragment. This enables the text fast path to activate on pages using{% extends %}where Django resolves blocks. -
Faster change detection —
_snapshot_assignsuses identity + shallow fingerprints (id, length, content hash for list-of-dicts) instead ofcopy.deepcopy. Framework-internal keys (csrf_token,kwargs,temporary_assigns,DATE_FORMAT,TIME_FORMAT) and auto-generated_countkeys are excluded fromset_changed_keysto avoid spurious re-renders. -
Optimized VNode parser — Pre-sized attribute HashMap, eliminated redundant
to_lowercase()call, removed form element debug output.
Fixed
-
Derived immutable context values no longer go stale on partial re-render —
_sync_state_to_rustpreviously skipped id()-based change detection for immutable types (int/str/bool/bytes) to avoid false positives from Python's int cache, which meant derived values computed inget_context_data(e.g.completed_count = sum(...),total_count = len(...)) were never synced to Rust when their sources changed. Partial rendering would then reuse the cached HTML for template nodes depending on those values, leaving counters stale after add/toggle/delete. Fixed by tracking previous VALUES for immutable keys and comparing by equality. Regression tests intest_changed_tracking.py::TestDerivedImmutableSync. -
VDOM input value leak on name change — When the patcher morphs an input into a different field (e.g., wizard step 1 name → step 2 email), the old field's typed value no longer leaks into the new field. Both
morphElementandSetAttrpatches now clear.valuewhen thenameattribute changes. -
In-place dict mutation detection —
_snapshot_assignsnow fingerprints list contents (id + dict values hash) to detect mutations liketodo['completed'] = Truethat don't change the list's id or length. Falls back to id-only for unhashable values. -
Derived context value detection — When
_changed_keysis set, the sync also checks non-immutable context values by id() to catch derived values (e.g.,productsfrom_products_cache) that change via private attributes.
[0.4.4] - 2026-04-15
Changed
-
Remove double
updateHooks()/bindModelElements()scanning — These were called in bothapplyPatches()andreinitAfterDOMUpdate(), scanning the full DOM twice per patch cycle. Removed fromapplyPatches(). Saves ~5ms per event. -
Delegated scoped listeners (dj-window-, dj-document-) — Replaced
querySelectorAll('*')full DOM scan with a registry-based delegation pattern. Scoped elements are scanned once at mount time and registered in a Map. Event listeners on window/document dispatch to the registry. Handles dotted attribute variants (dj-window-keydown.escape). -
Use
orjson.loads()for patch JSON parsing — 2-3x faster than stdlibjson.loads()when orjson is installed. Falls back gracefully. -
Gate debug payload behind panel open state —
get_debug_update()(dir + getattr + json.dumps per attribute) only runs when the debug panel is actually open, not on every event in DEBUG mode. Saves ~2-5ms per event. Panel sendsdebug_panel_open/debug_panel_closeWS messages on toggle.
[0.4.4rc1] - 2026-04-15
Fixed
-
VDOM patch path traversal skips regular HTML comments (#729) — The JS patcher was counting all HTML comment nodes during path traversal, but the Rust VDOM parser only preserves
<!--dj-if-->placeholders. This caused every page with HTML comments indj-rootto fail VDOM patching and fall back to full HTML recovery. -
Scroll to top on
dj-navigatelive_redirect —handleLiveRedirect()now scrolls to the top of the page (or to anchor if URL has a hash) afterpushState.
Changed
- Event delegation replaces per-element binding (#730) —
bindLiveViewEvents()no longer scans the DOM after every VDOM patch. Instead, one listener per event type is installed on thedj-rootelement via delegation (e.target.closest('[dj-click]')). This reduces client-side post-patch handling from ~56ms to ~30ms on large pages. Per-element rate limiting preserved via WeakMap.
Added
- Per-phase Rust timing in
render_with_diff()(#730) — Instrumentation measuring template render, html5ever parse, VDOM diff, and HTML serialization. Exposed to Python viaget_render_timing()and propagated to WebSocket response performance metadata.
[0.4.3] - 2026-04-14
Fixed
-
{% csrf_token %}no longer renders poisonedCSRF_TOKEN_NOT_PROVIDEDplaceholder (#696) — The Rust template engine now renders an empty string when no CSRF token is in context (instead of a placeholder that poisoned client.js's CSRF lookup). Python LiveView_sync_state_to_rust()now injects the real token fromget_token(request). Three-layer defense-in-depth fix merged as PR #708. -
HTTP fallback POST no longer replaces page with logged-out render (#705) — The POST handler now applies
_apply_context_processors()beforerender_with_diff()so auth context (user, perms, messages) is available during re-render. Context processor cleanup uses_processor_context()context manager for guaranteed cleanup. Merged as PR #710 + #714 + #721. -
Rust
|dateand|timefilters honor DjangoDATE_FORMAT/TIME_FORMATsettings (#713) — Newapply_filter_with_context()checks the template context for format settings when no explicit format argument is given. Python injects Django settings into the Rust context during_sync_state_to_rust(). Merged as PR #714. -
Rust
|datefilter now works onDateFieldvalues (#719) — The|datefilter previously only parsed RFC 3339 datetime strings.DateFieldvalues (bare dates like "2026-03-15") are now parsed via aNaiveDatefallback pinned to midnight UTC. Merged as PR #720. -
CSRF token value HTML-escaped in Rust renderer (#722) — The CSRF hidden input now uses the shared
filters::html_escape()utility (escaping &, ", <, >, and single quotes) instead of a manual.replace()chain that missed single quotes. Defense-in-depth. Merged as PR #727. -
Bare
except: passin CSRF injection now logs a warning (#716) — The CSRF token injection in_sync_state_to_rust()previously swallowed all exceptions silently. Now logs viadjust.rust_bridgelogger withexc_info=True. Merged as PR #721.
Changed
-
Context processor cleanup refactored to
_processor_context()context manager (#717) — Replaced the manual try/finally in the HTTP fallback POST handler with a reusable@contextmanagerthat guarantees cleanup of temporarily injected view attributes. Merged as PR #721 + #727. -
Pre-existing test fixes —
test_debug_state_sizescorrected forjson.dumps(default=str)behavior and\uXXXXescaping.navigation.test.jssuppresses happy-dom/undici WebSocket mockdispatchEventincompatibility.
Added
-
Python integration tests for DATE_FORMAT settings injection (#718) — 4 tests verifying
_sync_state_to_rustinjects DATE_FORMAT/TIME_FORMAT from Django settings. Merged as PR #721. -
Negative tests for
|datefilter invalid input (#725) — 4 Rust tests covering invalid dates, non-date strings, empty strings, and partial dates (filter returns original value per Django convention). Merged as PR #727. -
format_date()doc comment documenting Django compatibility (#726) — Documents supported input formats (RFC 3339, YYYY-MM-DD) and unsupported types (epoch ints, locale strings). Merged as PR #727.
[0.4.2] - 2026-04-13
Fixed
-
Derived context vars synced when parent instance attr mutated in-place (#703) —
_sync_state_to_rust()now collectsid()s of all sub-objects reachable from changed instance attrs and includes any derived context var whoseid()appears in that set. Previously, context vars computed inget_context_data()that returned sub-objects of a mutated dict (e.g.,wizard_step_data.get("person", {})) were skipped because theirid()was unchanged, causing templates to render stale data. Depth-capped at 8 with cycle detection. 9 new regression tests. -
as_live_field()now respectswidget.input_typeoverride fortypeattribute (#683 re-open) — The initial #683 fix mergedwidget.attrsbuttypewas still ignored because Django movestype=fromattrsintowidget.input_typeduring widget__init__._get_field_type()now checkswidget.input_typeagainst the widget class's default and uses the override when they differ (e.g.TextInput(attrs={"type": "tel"})setsinput_type="tel"). 4 new regression tests coveringtype="tel",type="url",type="search", and the defaulttype="text"fallback.
Added
- LiveComponent events now propagate to parent LiveView waiters (ADR-002 Phase 1b/1c follow-up) — Closes the "known limitation" documented in the v0.4.2 tutorials guide:
await self.wait_for_event("foo")on a LiveView now resolves when the matching handler fires on an embeddedLiveComponent, not just when it fires on the view itself. Without this, aTutorialStep(wait_for="submit", ...)wheresubmitis a handler on a childFormComponentwould silently stall forever — the parent view's waiter would never resolve and the tour would hang. The fix is in the WebSocket consumer'shandle_eventcomponent-event branch: after the component handler runs, the consumer now callsself.view_instance._notify_waiters(event_name, notify_kwargs)with the handler's kwargs + an injectedcomponent_idkey, mirroring the notification that already happened in the main LiveView branch from Phase 1b. Thecomponent_idinjection means apps can use the waiter'spredicateargument to disambiguate events fired from multiple component instances:wait_for_event("submit", predicate=lambda kw: kw.get("component_id") == "project_form"). A notification failure is caught and logged via thedjust.websocketlogger so a buggy waiter/predicate can't break the component handler's observable behavior — the component's state mutations always happen even if the waiter notification raises. 5 new regression tests inpython/tests/test_waiter_component_propagation.pycovering: component event resolves parent waiter,component_idis injected into notify kwargs so predicates can filter by source, multiple parent waiters for the same event all resolve (fan-out), the non-component branch still notifies parent waiters (regression guard for the Phase 1b path), and a raising_notify_waitersis logged-and-swallowed rather than propagating.docs/website/guides/tutorials.mdLimitations section updated to document the new behavior with acomponent_idpredicate example.
Documentation
-
Tutorial bubble must be placed outside
dj-root(#699) — If the{% tutorial_bubble %}tag is placed inside thedj-rootcontainer, morphdom recovery (which replaces the entiredj-rootcontent on patch failure) destroys the bubble mid-tour, causing it to silently disappear. The tutorials guide now has a dedicated "Bubble Placement" section explaining the requirement, why it exists, and correct/incorrect examples. The simplest-possible example at the top of the guide is updated to show the bubble outsidedj-root. Thetutorial_bubbletemplate tag docstring is also updated with this requirement. -
data-*attribute naming convention documented in Events guide (#623) — Howdata-foo-baron an HTML element maps tofoo_barin the event handler's kwargs was undocumented. The Events guide now has a dedicated "Data Attribute Naming Convention" section covering: the dash-to-underscore rule, client-side type-hint suffixes (:int,:float,:bool,:json,:list), server-side Python type-hint coercion, thedj-value-*alternative, which internaldata-*attributes are excluded, and a quick-reference table.
Changed
-
System checks T002, V008, C003 now suppressible via
DJUST_CONFIG(#603) — These three informational checks fire on everymanage.pyinvocation and are noisy for projects that deliberately don't use the checked features (daphne, explicitdj-root, non-primitive mount state). A newsuppress_checksconfig key inDJUST_CONFIG(orLIVEVIEW_CONFIG) accepts a list of check IDs to silence:DJUST_CONFIG = {"suppress_checks": ["T002", "V008", "C003"]}. Both short IDs ("T002") and fully-qualified IDs ("djust.T002") are accepted, case-insensitive. Only the informational/advisory variants are suppressed — the C003 Warning (daphne misordered) still fires because it indicates a real misconfiguration. 7 new tests for the suppression mechanism. -
release-drafter/release-drafterv6 → v7 + droppull_requesttrigger — v7 validatestarget_commitishagainst the GitHub releases API and rejectsrefs/pull/<n>/mergerefs, which is whatgithub.refresolves to under apull_requesttrigger. v6 silently tolerated this; v7 does not, causing every PR to fail withValidation Failed: target_commitish invalid. The fix is to drop thepull_requesttrigger — release-drafter is designed to track changes that have landed on the release branch, not comment on in-flight PRs, sopush: branches: [main]is the right fit. Aligns with how Phoenix, Elixir, GitHub CLI, and other major projects wire release-drafter. Resolves the v7 bump that was deferred out of the v0.4.2 dependabot batch (#680). -
Dependency batch carry-over (v0.4.2) — Drains the dependabot backlog that was held behind the v0.4.1 release. Single consolidated PR so one CI run catches any inter-dep interactions:
- npm:
vitest/@vitest/ui/@vitest/coverage-v84.0.18 → 4.1.4 (patches + new test runner features),jsdom29.0.1 → 29.0.2,happy-dom20.8.4 → 20.8.9. Full JS suite remains green (1111 tests). - Cargo:
tokio1.50 → 1.51 (workspace),uuid1.22 → 1.23,proptest1.10 → 1.11 (djust_vdom),indexmap2.13.0 → 2.14.0 (transitive pickup via cargo update).cargo check --workspaceclean;cargo test -p djust_vdompasses all 42 proptest-driven tests on the new 1.11 runtime. - GitHub Actions:
actions/github-scriptv8 → v9 (two workflows),astral-sh/setup-uvv6 → v7 (test workflow). Workflow syntax unchanged. - Intentionally deferred:
html5ever0.36 → 0.39 is a 3-minor-version jump that requires a matchingmarkup5ever_rcdom0.39 release which has not yet been published to crates.io (only git snapshots exist in the html5ever workspace). Using git deps in our published workspace would breakcargo publishand leak unreleased upstream state, so this stays deferred until upstream publishes.release-drafter/release-drafterv6 → v7 was also deferred out of this chore batch because of atarget_commitishvalidation incompatibility — shipped as a separate follow-up PR alongside this one.
Closes 13 open dependabot PRs as superseded (#581, #582, #604, #606, #607, #609, #615, #616, #644, #645, #646, #647, #648).
- npm:
Fixed
-
@backgroundnatively supportsasync defhandlers (#697) — The@backgrounddecorator now detectsasyncio.iscoroutinefunctionand creates a native async closure so_run_async_workcanawaitit directly on the event loop instead of routing throughsync_to_async. The fragileinspect.iscoroutine(result)workaround from #692 is kept as a legacy fallback. 5 new regression tests. -
flush_push_events()resolves callback dynamically on WS reconnect (#698) —PushEventMixin.flush_push_events()now resolves the flush callback viaself._ws_consumer._flush_push_eventsat call time instead of relying on a stored_push_events_flush_callbackthat was only wired during initial mount. After a WebSocket reconnect the view instance is restored from session but the stored callback was stale. The dynamic lookup always finds the current consumer. Legacy stored callback kept as fallback. 7 new tests. -
push_commands-only handlers auto-skip VDOM re-render (#700) — Handlers that only call
push_commands()/push_event()without changing public state no longer trigger a VDOM re-render. The_snapshot_assignsdeep-copy comparison could report false positives for views with non-copyable public attributes (querysets, file handles) because sentinel objects never compare equal. A new identity-based check (id()comparison before/after) detects whether any public attribute was actually rebound and auto-sets_skip_render = Truewhen push events are pending but no state changed. 5 new tests. -
System check V010 detects wrong TutorialMixin MRO ordering at startup (#691) — Django's
View.__init__does not callsuper().__init__(), so writingclass MyView(LiveView, TutorialMixin)silently skips TutorialMixin's initialisation. A newdjust.V010system check scans all LiveView subclasses at startup and emits an Error with a clear fix hint when TutorialMixin appears after a View-derived base in the class declaration. Suppressible viaDJUST_CONFIG = {"suppress_checks": ["V010"]}. 5 new tests. Tutorials guide updated with correct ordering. -
@background async defhandlers now execute correctly (#692) —@backgroundwraps handlers in a sync closure; when the handler isasync def, the closure returned an unawaited coroutine and the handler body never ran. The fix in_run_async_work(already on main via workaround) detects coroutine returns and awaits them. 11 new regression tests intest_background_async.pyverify both sync and async handlers execute their bodies. -
push_commandsin@backgroundtasks now flush mid-execution (#693) — Push events queued bypush_commandsinside a@backgroundhandler only reached the client when the entire task completed. The_flush_pending_push_eventscallback mechanism (already on main) lets TutorialMixin and other background handlers flush events immediately. A new publicawait self.flush_push_events()method on PushEventMixin provides the same capability to any@backgroundhandler. 7 new tests intest_push_flush_background.py. -
get_context_datano longer includes non-serializable class attributes (#694) — The MRO walker inContextMixin.get_context_data()added class-level attributes (liketutorial_steps) to the template context. Non-JSON-serializable values were silently converted to theirstr()repr, corrupting state on subsequent events. The fix skips class-level attributes that fail a JSON serialisability probe. Additionally,TutorialMixinnow stores steps as_tutorial_steps(private) with a read-onlytutorial_stepsproperty, so they are excluded by both the_prefix convention and the serialisability check. 14 new tests. -
Debug panel SVG attributes no longer double-escaped (#613) — SVG attributes like
viewBoxandpath din the debug toolbar were rendered garbled because the Rust VDOM'sto_html()method HTML-escaped text content inside<script>and<style>elements. Per the HTML spec, these are "raw text elements" whose content must be emitted verbatim — escaping&to&or<to<corrupts JavaScript/CSS code and causes double-escaping when the HTML is round-tripped through the VDOM pipeline (parse with html5ever which decodes entities, then re-serialize withto_html()which re-encodes them). The fix adds anin_raw_textflag to the internal_to_html()serializer that propagates through<script>/<style>children, skippinghtml_escape()for their text nodes. SVG attribute values in templates (which don't contain HTML special characters) were already correct but now have explicit regression tests. 4 new Rust unit tests, 3 new Rust integration tests (script/style/SVG roundtrip), 3 new Python regression tests (JS source validation, JSON injection check, VDOM roundtrip), and 3 new JS tests (tab icon SVGs, path d attributes, header button SVGs all verified in DOM). -
form.cleaned_dataPython types no longer serialize to null (#628) —datetime.date,datetime.datetime,datetime.time,Decimal, andUUIDvalues inform.cleaned_datastored in public view state are now properly serialized to their JSON representations (ISO strings, floats, strings) instead of silently becomingnull. Both theDjangoJSONEncoderandnormalize_django_value()already handled these types; 10 new regression tests confirm the behavior. -
set()is now JSON-serializable as public state (#626) — Storing a Pythonset()orfrozenset()in public view state no longer crashesjson.dumps. Sets are serialized as sorted lists (falling back to unsorted when elements aren't comparable). BothDjangoJSONEncoder.default()andnormalize_django_value()now handleset/frozenset. 11 new regression tests. -
dictstate no longer corrupted tolistafter Rust state sync (#612) — Round-tripping state through the Rust MessagePack serialization boundary could corruptdictvalues intolistbecause#[serde(untagged)]on theValueenum letrmp_serdematch a msgpack map against theListvariant before tryingObject. The fix replaces the derivedDeserializewith a custom visitor-based implementation that uses the deserializer's type hints (visit_mapvsvisit_seq) to correctly distinguish maps from arrays. 4 new Rust regression tests + 1 Python end-to-end msgpack round-trip test. -
as_live_field()now mergeswidget.attrsinto rendered HTML (#683) — Theas_live_field()method (and{% live_field %}tag) dropped any attributes defined on a Django widget'sattrsdict —type="email",placeholder,pattern,min/max, customdata-*, and any other HTML attributes were silently lost. The fix adds_merge_widget_attrs()toBaseAdapter, called from_render_input,_render_checkbox, and_render_radio, which mergesfield.widget.attrsinto the output attributes with djust-specific keys (dj-change,name,class, etc.) taking precedence over widget defaults. BooleanFalse/Nonevalues in widget attrs are filtered out to avoid renderingdisabled="False". 17 new regression tests inpython/tests/test_live_field_widget_attrs.pycovering: EmailInput placeholder/type, pattern/min/max/step/title, djust attrs override clashing widget attrs, empty widget attrs, textarea rows/cols, checkbox data-attrs, radio data-attrs on each option, select data-attrs, and boolean True/False handling. -
VDOM patcher guards against text nodes for 5 patch types (#622) — The VDOM diff patcher called
setAttribute(),removeAttribute(),appendChild(),removeChild(), andreplaceChild()on#textnodes, which don't implement these methods. This crashed conditional rendering whenever a text node sat where the patcher expected an element (common in{% if %}blocks that switch between text and element content). The fix adds anisElement(node)guard at the top of each of the five patch-type branches in12-vdom-patch.js— when the target is a non-element node (text, comment, CDATA), the patch is skipped gracefully instead of throwing. 4 new JS tests intests/js/vdom_patch_errors.test.jscovering setAttribute, removeAttribute, appendChild, and replaceChild on text nodes. -
Autofocus handling on dynamically inserted elements (#617) — Dynamically inserted
<input autofocus>elements didn't receive focus after a VDOM patch because the browser only honours theautofocusattribute on initial page load. The patcher now detectsautofocuson newly inserted elements after each patch cycle and calls.focus()explicitly. 4 new JS tests intests/js/vdom-autofocus.test.jscovering single autofocus, multiple elements (last wins), elements without autofocus ignored, and no-op when no autofocus elements are present. -
Private
_attributes preserved across events and reconnects (#627, #611) — Two related state-management bugs caused any attribute starting with_(the documented convention for private/internal state) to be silently wiped. The root cause was that session save used the output ofget_context_data(), which by design strips_-prefixed attributes. For #627, every WebSocket event round-trip lost private state because_save_state_to_session()persisted only public context. For #611, the pre-rendered WS reconnect path restored session state but never included private attributes set during the HTTP GET mount. The fix adds two helpers —_get_private_state()(collects all_-prefixed instance attrs that aren't dunder or in the base-class exclusion set) and_restore_private_state(state_dict)— and wires them into_save_state_to_session()(now persists private state under a_private_statesession key) and_load_state_from_session()/ the reconnect path inRequestMixin._restore_session_state()(restores private attrs before the view resumes). 20 new regression tests inpython/tests/test_private_attr_preservation.pycovering: private attrs survive event dispatch, survive reconnect, survive multiple sequential events, coexist with public attrs, handle None/complex/nested values, are excluded for dunder attrs, are excluded for base-class internals, and round-trip through session save/load. -
Layout flash on pre-rendered mount: defer
reinitAfterDOMUpdateviarequestAnimationFrame(#619, fixes #618) — Carry-over bugfix from v0.4.1. When a page is pre-rendered via HTTP GET, the WebSocket mount used to callreinitAfterDOMUpdate()synchronously right after stampingdj-idattributes onto the existing DOM. That synchronous call triggered a full DOM traversal for event binding, which forced the browser to recalculate layout mid-paint — and on pages with large pre-rendered elements (e.g. big dashboard stat values) the elements briefly rendered at the wrong size before settling, producing a visible layout-flash on every initial load. The fix moves the post-mount block (reinit +_mountReadyflag + form recovery + auto-recover) into arunPostMountclosure and schedules it viarequestAnimationFrame(runPostMount)when available, falling back to a synchronous call whenrequestAnimationFrameis unavailable (JSDOM tests, exotic non-browser environments). Event binding now happens after the browser finishes its current paint, eliminating the flash entirely. The ordering invariant (reinit →_mountReady→ form recovery) is preserved inside the closure sodj-mountedhandlers and recovered form inputs still see bound event listeners. The non-prerendereddata.htmlinnerHTML-replace branch is unchanged — it already invalidates layout via the full DOM swap so there's no pre-paint to protect. 8 new regression tests intests/js/mount-deferred-reinit.test.jsasserting: the rAF wrapper is present, the synchronous fallback is preserved, the closure is namedrunPostMountfor stable debugging,reinitAfterDOMUpdate()runs before_mountReadyinside the closure,_mountReadyis set inside the closure (not synchronously), form recovery runs only on reconnect inside the closure, the non-prerendered branch calls reinit synchronously, and exactly one call-site ofreinitAfterDOMUpdate()exists in the skipMountHtml branch (so a refactor that reintroduces the sync call would immediately flip red). Closes #619 as superseded and closes the original #618 bug report. -
Scaffolded projects now default
DEBUG=Falseand generate.env.example(#637) — Carry-over bugfix from v0.4.1. Previously,python -m djust startproject mysiteandpython -m djust new mysiteboth generated asettings.pywithDEBUG = TrueandALLOWED_HOSTS = ["*"]as hardcoded literals. A developer who deployed the scaffolded output without remembering to flip those values ran production with full stack traces, thedjango-insecure-<random>default SECRET_KEY, and a wildcard host allowlist — the exact footgun that A001 (DEBUGenabled) and A014 (ALLOWED_HOSTStoo permissive) flag indjust_audit. Now both scaffold paths (cli.py'scmd_startprojectand the higher-leveldjust.scaffolding.generator.generate_project) emitDEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1", "yes")andALLOWED_HOSTS = [host.strip() for host in os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") if host.strip()]— unconfigured deployments fail safe. The scaffold also writes a.env.exampletemplate alongside.gitignore(which already ignores.env) so local development picks up developer-friendly values viacp .env.example .env+ whatever.envloader the developer uses. The.env.exampleincludesDEBUG=True, a freshly-generatedSECRET_KEYtoken (viasecrets.token_urlsafe(50)), andALLOWED_HOSTS=localhost,127.0.0.1so the local experience hasn't changed. 4 new regression tests inpython/tests/test_cli_scaffold.pyasserting:DEBUG = Trueis no longer literal,DEBUGreads from env with"False"fallback,ALLOWED_HOSTS = ["*"]is no longer literal, narrowlocalhost,127.0.0.1env default,.env.exampleexists with the three documented vars and a real (not template-placeholder) secret key,.envremains in.gitignorewhile.env.exampledoes not. Closes #637.
Added
-
TutorialMixin+TutorialStep+{% tutorial_bubble %}— declarative guided tours (ADR-002 Phase 1c) — Capstone of ADR-002 Phase 1: a one-import, zero-JavaScript way for any djust app to ship a real guided tour, onboarding flow, or wizard. Apps declare the tour as a list ofTutorialStepdataclasses on aLiveViewthat mixes inTutorialMixin; the framework runs the state machine as a@backgroundtask, pushing a highlight + narrate + focus chain at each step's target viapush_commands(Phase 1a), then eitherasyncio.sleep'ing for auto-advance steps orawaitingwait_for_event(Phase 1b) until the user actually fires the matching@event_handler. Four event handlers come for free —start_tutorial,skip_tutorial,cancel_tutorial,restart_tutorial— along with three instance attributes (tutorial_running,tutorial_current_step,tutorial_total_steps) for progress display in the view state.TutorialStepsupports per-steptarget(CSS selector, required),message(narration text),position(top/bottom/left/rightbubble hint),wait_for(handler name to suspend on),timeout(seconds — pairs withwait_forfor bounded waits or used alone for auto-advance),on_enter/on_exit(optional extraJSChainpushes for per-step setup/teardown beyond the default highlight + narrate + focus), andhighlight_class/narrate_event(override per-step CSS class and CustomEvent name when you need different visual treatment). Skip and cancel signals are raced against the wait viaasyncio.wait(..., return_when=FIRST_COMPLETED)so either unblocks the current step immediately; WebSocket disconnect cancels the background task automatically so there's no lingering work, no leaked waiters, no stuck highlights. A new{% tutorial_bubble %}template tag renders a floating narration bubble that listens fortour:narrateCustomEvents atdocumentlevel (dispatched at the step's target withbubbles: true), positions itself next to the target per the step'spositionhint, displaysstep N / totalprogress, and includes "Skip" and "Close" buttons pre-bound to the mixin's event handlers — the default bubble is markeddj-update="ignore"so morphdom doesn't clobber it during VDOM patches. The new client-sidesrc/28-tutorial-bubble.jsmodule (~140 lines, bringsclient.jsto 30 modules) registers its listeners unconditionally at IIFE time, readsdetail.text/target/position/step/totalfrom the event, and updates the bubble's text + progress + position + visibility. The framework ships no CSS — apps style the bubble and highlight class themselves (the guide includes a minimal starter block). 26 new Python tests for the mixin covering TutorialStep dataclass (minimal, custom position, invalid position, empty target, empty message, wait_for+timeout, on_enter/on_exit), lifecycle (initial state, empty-steps no-op, single step, setup+cleanup chain order, multi-step order, idempotent start-while-running),wait_for_eventintegration (step suspends on user action, timeout advances silently, indefinite wait), skip/cancel paths (advance past current, abort loop, no-op when not running),on_enter/on_exitpushes, per-step highlight class override, and per-step narrate event override. 9 new Python tests for thetutorial_bubbletemplate tag covering defaults, customcss_class/event/position, invalid-position fallback to"bottom", skip+cancel button bindings, text/progress element classes, and XSS escaping of hostilecss_classandeventkwargs. 12 new JS tests intests/js/tutorial-bubble.test.jscovering listener registration, text content updates, progress text updates, show/hide viadata-visible, default/custom position application, missing-target graceful handling, missing-bubble graceful handling,tour:hideevent, and repeated updates on subsequent events. Zero new runtime dependencies — stdlibasyncio+dataclasses+ Django'sformat_html. Full documentation in the newdocs/website/guides/tutorials.mdguide with the simplest-possible example, state-machine description,TutorialStepreference,wait_for/timeoutcombinations table,on_enter/on_exitpatterns, the bubble template tag docs, a starter CSS block, four usage patterns (auto-advance walkthrough, interactive onboarding, mixed, branching with custom handlers), skip/cancel UX, disconnect cleanup, debugging tips, and honest limitations (LiveComponent events don't propagate to parent waiters yet, actor-mode views bypass the dispatch hook, handler validation failures prevent the waiter from resolving except via timeout, single-user only — multi-user broadcast is Phase 4 in v0.5.x). -
await self.wait_for_event(name, timeout=None, predicate=None)async primitive (ADR-002 Phase 1b) — Second half of the backend-driven UI Phase 1 primitives. Adds a newWaiterMixin(automatically included inLiveView) that lets a@backgroundhandler suspend until a specific@event_handleris called by the user, optionally filtered by a predicate, optionally bounded by a timeout. The returned dict is the kwargs that were passed to the matching handler. This is the primitive that makes "highlight this button, wait for the user to actually click it, then advance to the next step" work declaratively — required byTutorialMixin(Phase 1c) and by any server-driven flow that needs to pause mid-plan until real user input arrives. Implementation: ~180 lines inpython/djust/mixins/waiters.py, a ~15-line hook inpython/djust/websocket.pythat calls_notify_waitersafter every successful handler invocation, a ~10-line cleanup hook in the WebSocketdisconnectpath that cancels all pending waiters when the view tears down (so@backgroundtasks unblock withCancelledErrorinstead of leaking), and proper integration intoLiveView's MRO viapython/djust/mixins/__init__.py. The notify pass runs AFTER the handler completes so waiters created during a handler call aren't self-notified (prevents re-entrancy surprises wherewait_for_event("X")inside anXhandler would resolve against itself). Multiple concurrent waiters for the same event name all resolve with the same kwargs dict when that event fires — fan-out patterns work without manual coordination. Waiters for different event names are fully independent. A predicate that raises is treated as "no match" and logged via thedjust.waiterslogger, so a buggy predicate can't crash the event pipeline or deadlock a background task. 18 new Python tests covering: basic resolution, kwargs copy semantics, no-op on unmatched names, predicate filtering, predicate-that-raises treated as False with warning log, predicate=None matches any kwargs, timeout raisesasyncio.TimeoutError, expired waiters removed from registry, indefinite waits without timeout, concurrent waiters for same event all resolve, waiters for different events are independent, partial resolution (some predicates match, others don't),_cancel_all_waitersunblocks pending futures withCancelledErrorand clears the registry, task cancellation removes the waiter, and stability under mid-iteration waiter-list mutation. Full documentation in the existingdocs/website/guides/server-driven-ui.mdguide with signature, predicate examples, concurrency semantics, timeouts and cleanup, composition withpush_commands, and honest limitations (no component-event support yet, actor mode bypasses the hook, validation failures prevent handler execution which means waiters never resolve except via timeout). -
LiveView.push_commands(chain)+djust:execclient-side auto-executor (ADR-002 Phase 1a) — First half of the backend-driven UI primitives proposed in ADR-002. Adds a one-line server-side helperself.push_commands(chain)that takes adjust.js.JSChain(shipped in v0.4.1 as the JS Commands fluent API) and pushes it to the current session as adjust:execpush event carrying the chain's JSON-serializedopslist. The client half is a new framework-providedsrc/27-exec-listener.jsmodule that listens fordjust:push_eventCustomEvents onwindow, filters forevent === 'djust:exec', and runs the ops viawindow.djust.js._executeOps(ops, document.body)— the same function that runs inlinedj-click="[[...]]"JSON chains and fluent-API.exec()calls from hook code. No hook registration, no template markup, no user setup required: the auto-executor ships bound withclient.jsand is active on every djust page automatically. The server-side helper is type-safe — it rejects anything that isn't aJSChainwith a clearTypeErrorpointing at theJS.*factory methods, preventing raw ops-list smuggling through thepush_eventpath.push_commandsandpush_eventshare the same queue and preserve ordering, so handlers can interleave "push a flash message, add a CSS class, fire analytics, run an animation" in one deterministic sequence. 23 new Python tests covering single-op chains, multi-op ordering, empty chains, JSON round-trip, immutability of chains after push, type validation against strings/dicts/lists/None, queue composition withpush_event, and per-op factory parity across all 11 JS Commands. 13 new JS tests intests/js/exec-listener.test.jscovering listener registration, single-op execution, multi-op ordering, multiple-classadd_class,focus,dispatchwith detail, filtering for non-djust:execevents, malformed-payload rejection (missingops, non-arrayops, missing detail), error resilience (one bad op doesn't break the chain), multiple independent exec fires, and end-to-end integration with the fluentwindow.djust.jschain factory. Zero new runtime dependencies. Full documentation indocs/website/guides/server-driven-ui.mdwith patterns, debugging tips, and pointers to Phase 1b (wait_for_event) and Phase 1c (TutorialMixin) still to come in v0.4.2.
[0.4.1] - 2026-04-11
Added
-
JS Commands — client-side DOM commands chainable from templates, views, hooks, and JavaScript — Closes the single biggest DX gap vs Phoenix LiveView 1.0. Eleven commands (
show,hide,toggle,add_class,remove_class,transition,dispatch,focus,set_attr,remove_attr,push) that run locally without a server round-trip, plus apushescape hatch that mixes in server events when needed. Four equivalent entry points: (1) Python helperdjust.js.JS— fluent chain builder that stringifies to a JSON command list, wrapped inSafeStringfor safe template embedding (<button dj-click="{{ JS.show('#modal').add_class('active', to='#overlay') }}">Open</button>). (2) Client-sidewindow.djust.js— mirror of the Python API withcamelCasemethod names for direct JavaScript use (window.djust.js.show('#modal').addClass('active', {to: '#overlay'}).exec()). (3) Hook API — everydj-hookinstance now has athis.js()method returning a chain bound to the hook element (Phoenix 1.0 parity for programmable JS Commands from hook lifecycle callbacks). (4) Attribute dispatcher —dj-click(and other event-binding attributes) detect whether the attribute value is a JSON command list ([[...]]) and execute it locally; plain handler names still fire server events as before (zero breaking changes). All commands support scoped targets:to=<selector>(absolutedocument.querySelectorAll),inner=<selector>(scoped to origin element's descendants),closest=<selector>(walk up the DOM from origin) — a single<button dj-click="{{ JS.hide(closest='.modal') }}">Close</button>works in every modal with no per-instance IDs. Thepushcommand acceptspage_loading=Trueto show the navigation-level loading bar while the event round-trips. Chains are immutable — every chain method returns a newJSChain, so reusing a base chain across multiple call sites never cross-contaminates. 37 new Python tests (every command + target validation + chain immutability + HTML/SafeString integration + template rendering) and 30 new JS tests (every command executing against real DOM + target resolution + chain fluency + attribute dispatcher + backwards-compat for plain event names +parseCommandValueedge cases). Zero new dependencies — the Python helper is stdlib-only and the JS interpreter is ~350 lines in a newsrc/26-js-commands.jsmodule. Full guide indocs/website/guides/js-commands.mdwith examples for templates, hooks, chaining, and the "when to reach for what" decision tree. -
dj-paste— paste event handling — New attribute that fires a server event when the user pastes content into a bound element (<textarea dj-paste="handle_paste">). The client extracts structured payload from theClipboardEventin one pass:text(clipboardData.getData('text/plain')),html(getData('text/html')for rich paste from Word/Google Docs/web pages),has_files(bool), andfiles(list of{name, type, size}metadata dicts for every file inclipboardData.files). When the element also carries adj-upload="<slot>"attribute, the clipboard'sFileListis routed through the existing upload pipeline — image-paste → chat, CSV-paste → table, etc. — via a newwindow.djust.uploads.queueClipboardFiles(element, fileList)export. Participates in the standard interaction pipeline (dj-confirm,dj-lock). By default the browser's native paste still happens so hybrid editors feel natural; adddj-paste-suppressto intercept fully (useful when routing image paste to an upload slot without dumping a data URL into a<div contenteditable>). Positional args in the attribute syntax (dj-paste="handle_paste('chat', 42)") forward viakwargs["_args"]. 11 new JS tests covering text extraction, HTML extraction, file metadata, suppress flag, missingclipboardData, double-bind protection, positional args, upload routing with and without adj-uploadslot, and graceful degradation whengetData('text/html')throws. ~80 lines JS. Full guide indocs/website/guides/dj-paste.md. -
djust_audit --ast— AST security anti-pattern scanner (#660) — Adds a new mode todjust_auditthat walks the project's Python source and Django templates looking for five specific security anti-patterns, each motivated by a live vulnerability or near-miss in the 2026-04-10 NYC Claims penetration test. Seven stable finding codesdjust.X001–djust.X007: X001 (ERROR) — possible IDOR:Model.objects.get(pk=...)inside a DetailView / LiveView without a sibling.filter(owner=request.user)(oruser=,tenant=,organization=,team=,created_by=,author=,workspace=) scoping the queryset. X002 (WARN) — state-mutating@event_handlerwithout any permission check (no class-levellogin_required/permission_required, no@permission_required/@login_required). X003 (ERROR) — SQL string formatting:.raw()/.extra()/cursor.execute()passed an f-string, a.format()call, or a"..." % ...binary-op. X004 (ERROR) — open redirect:HttpResponseRedirect(request.GET[...])/redirect(...)without anurl_has_allowed_host_and_schemeoris_safe_urlguard in the enclosing function. X005 (ERROR) — unsafemark_safe/SafeStringwrapping an interpolated string (XSS risk). X006 (WARN) — template uses{{ var|safe }}(regex scan of.htmlfiles). X007 (WARN) — template uses{% autoescape off %}. Suppression via# djust: noqa X001on the offending line, or{# djust: noqa X006 #}inside templates. New CLI flags:--ast,--ast-path <dir>,--ast-exclude <prefix> [...],--ast-no-templates. Supports--jsonand--strict(fail on warnings too). 52 new tests covering positive + negative cases for every checker, management-command integration, template scanning, and noqa suppression. Zero new runtime dependencies — stdlibast+re. Full documentation indocs/guides/djust-audit.mdanddocs/guides/error-codes.md#ast-anti-pattern-scanner-findings-x0xx. Closes the v0.4.1 audit-enhancement batch (#657/#659/#660/#661 all shipped). -
New consolidated
djust_auditcommand guide —docs/guides/djust-audit.mddocuments all five modes of the command (default introspection,--permissions,--dump-permissions,--live,--ast), every CLI flag, CI integration examples, and exit-code conventions. Cross-linked fromdocs/guides/security.md. -
Error code reference expanded with 44 new codes —
docs/guides/error-codes.mdnow covers the A0xx static audit checks (7 codes: A001, A010, A011, A012, A014, A020, A030), the P0xx permissions-document findings (7 codes: P001–P007), and the L0xx runtime-probe findings (30 codes: L001–L091). Every code gets severity, cause, fix, and a reference to the related issue/PR. -
{% live_input %}template tag — standalone state-bound form fields for non-Form views (#650) —FormMixin.as_live_field()andWizardMixin.as_live_field()render form fields with proper CSS classes,dj-input/dj-changebindings, and framework-aware styling — but only for views backed by a DjangoFormclass. This leaves non-form views (modals, inline panels, search boxes, settings pages, anywhere state lives directly on view attributes) without an equivalent helper. The new{% live_input %}tag fills this gap with a lightweight alternative that needs noFormclass orWizardMixin. Supports 12 field types (text,textarea,select,password,email,number,url,tel,search,hidden,checkbox,radio), explicitevent=override (defaults sensibly per type —text→dj-input,select/radio/checkbox→dj-change,hidden→ none),debounce=/throttle=passthrough, framework CSS class resolution viaconfig.get_framework_class('field_class'), HTML attribute passthrough with underscore-to-dash normalisation (aria_label="Search"→aria-label="Search"), and a tested XSS escape boundary via a new shareddjust._html.build_tag()helper. Example:{% live_input "text" handler="search" value=query debounce="300" placeholder="Search..." %}. 56 new tests including an explicit XSS matrix across every field type and attribute. Seedocs/guides/live-input.mdfor the full setup guide. -
djust_audit --live <url>— runtime security-header and CSWSH probe (#661) — Adds a new mode todjust_auditthat fetches a running deployment with stdliburlliband validates security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, CORP), cookies (HttpOnly, Secure, SameSite on session/CSRF cookies), information-disclosure paths (/.git/config,/.env,/__debug__/,/robots.txt,/.well-known/security.txt), and optionally probes the WebSocket endpoint withOrigin: https://evil.exampleto verify the CSWSH defense from #653 is actually enforced end-to-end. This catches the class of production issues where the setting is correctly configured insettings.pybut the response is stripped, rewritten, or never emitted by the time it reaches the client — the NYC Claims pentest caught a criticalContent-Security-Policy missingcase this way (django-cspwas configured but the header was absent from production responses, stripped by an nginx ingress). 30 new stable finding codesdjust.L001–djust.L091cover every check class so CI configs can suppress specific codes by number. New CLI flags:--live <url>,--paths(multi-URL),--no-websocket-probe,--header 'Name: Value'(for staging auth),--skip-path-probes(for WAF-protected environments). Supports--jsonand--strict(fail on warnings too). Zero new runtime dependencies — stdliburllibfor HTTP, optionalwebsocketspackage for the WebSocket probe (skipped with an INFO finding if not installed). -
New static security checks in
djust_check/djust_audit(#659) — Seven new check IDs fire fromcheck_configurationwhen Django runspython manage.py check: A001 (ERROR) — WebSocket router not wrapped inAllowedHostsOriginValidator(static-analysis companion to #653 for existing apps built from older scaffolds). A010 (ERROR) —ALLOWED_HOSTS = ["*"]in production. A011 (ERROR) —ALLOWED_HOSTSmixes"*"with explicit hosts (the wildcard makes the explicit entries meaningless). A012 (ERROR) —USE_X_FORWARDED_HOST=Truecombined with wildcardALLOWED_HOSTSenables Host header injection. A014 (ERROR) —SECRET_KEYstarts withdjango-insecure-in production (scaffold default not overridden before deployment). A020 (WARNING) —LOGIN_REDIRECT_URLis a single hardcoded path but the project has multiple auth groups/permissions (catches the "every role lands on the same dashboard" anti-pattern). A030 (WARNING) —django.contrib.admininstalled without a known brute-force protection package (django-axes,django-defender, etc.). Each check has essentially zero false-positive risk, has afix_hintpointing at the remediation, and was motivated by the 2026-04-10 NYC Claims pentest report. Out of scope for this PR: manifest scanning (k8s/helm/docker-compose env blocks) — deferred to a follow-up. Python-levelsettings.pyvalues cover the common case. -
djust_audit --permissions permissions.yaml— declarative permissions document for CI-level RBAC drift detection (#657) — Adds a new flag todjust_auditthat validates every LiveView against a committed, human-readable YAML document describing the expected auth configuration for each view. CI fails on any deviation (view declared public but has auth in code, permission list mismatch, undeclared view in strict mode, stale declaration, etc.). This closes a structural gap the existing audit couldn't catch:djust_audittoday can tell "no auth" from "some auth", but not thatlogin_required=Trueshould have beenpermission_required=['claims.view_supervisor']. The permissions document IS the ground truth. Seven stable error codes (djust.P001throughdjust.P007) cover every deviation class. Also adds--dump-permissionsto bootstrap a starter YAML from existing code, and--strictto fail CI on any finding. Full documentation indocs/guides/permissions-document.md. Motivated by NYC Claims pentest finding 10/11 where every view hadlogin_required=Trueset and djust_audit reported them all as protected, but the lowest-privilege authenticated user could ID-walk the entire database. -
WizardMixinfor multi-step LiveView form wizards — General-purpose mixin managing step navigation, per-step validation, and data collection for guided form flows. Providesnext_step,prev_step,go_to_step,update_step_field,validate_field, andsubmit_wizardevent handlers. Template context includes step indicators, progress, form data/errors, and pre-rendered field HTML viaas_live_field(). Re-validates all steps on submission to guard against tampered WebSocket replays. (#632)
Security
-
LOW: Nonce-based CSP support — drop
'unsafe-inline'fromscript-src/style-src— djust's inline<script>and<style>emissions (handler metadata bootstrap inTemplateMixin._inject_handler_metadata,live_sessionroute map inrouting.get_route_map_script, and the PWA template tagsdjust_sw_register,djust_offline_indicator,djust_offline_styles) now readrequest.csp_noncewhen available (set by django-csp whenCSP_INCLUDE_NONCE_INcovers the relevant directive) and emit anonce="..."attribute on the tag. When no nonce is available (django-csp not installed, orCSP_INCLUDE_NONCE_INnot set), the tags emit without a nonce attribute — fully backward compatible with apps still allowing'unsafe-inline'. Apps that want strict CSP can now setCSP_INCLUDE_NONCE_IN = ("script-src", "script-src-elem", "style-src", "style-src-elem")insettings.py, drop'unsafe-inline'fromCSP_SCRIPT_SRC/CSP_STYLE_SRC, and get strict CSP XSS protection across all djust-generated inline content. The PWA tagsdjust_sw_register,djust_offline_indicator, anddjust_offline_stylesnow usetakes_context=Trueto read the request from the template context — they still work with the same template syntax ({% djust_sw_register %}etc.) as long as aRequestContextis used (Django's default for template rendering). Seedocs/guides/security.mdfor the full setup. Reported via external penetration test 2026-04-10 (FINDING-W06). Closes the v0.4.1 security hardening batch (#653 / #654 / #655). (#655) -
MEDIUM: Gate VDOM patch timing/performance metadata behind
DEBUG/DJUST_EXPOSE_TIMING—LiveViewConsumerpreviously attachedtiming(handler/render/total ms) andperformance(full nested timing tree with handler and phase names) to every VDOM patch response unconditionally, regardless ofsettings.DEBUG. Combined with CSWSH (#653) this let cross-origin attackers observe server-side code-path timings, enabling timing-based code-path differentiation (DB hit vs cache miss, valid vs invalid CSRF), internal handler/phase name disclosure, and load-based DoS scheduling. Now gated on a new helper_should_expose_timing()which returns True only whensettings.DEBUGor the newsettings.DJUST_EXPOSE_TIMINGis True. Upgrade notes: production behavior change — existing clients that consumedresponse.timing/response.performancein production will no longer see those fields; opt in viaDJUST_EXPOSE_TIMING = Truein settings for staging/profiling. The browser debug panel is unaffected (it receives timing via the existing_attach_debug_payloadpath, which is already gated onDEBUG). Reported via external penetration test 2026-04-10. References: CWE-203, CWE-215, OWASP A09:2021. (#654) -
HIGH: Validate WebSocket Origin header to prevent Cross-Site WebSocket Hijacking (CSWSH) —
LiveViewConsumer.connect()previously accepted the WebSocket handshake without validating theOriginheader, andDjustMiddlewareStackdid not wrap the router in an origin validator. A cross-origin attacker could mount any LiveView and dispatch any event from a victim's browser. Now the consumer rejects disallowed origins with close code 4403 before accepting the handshake, andDjustMiddlewareStackwraps its inner application inchannels.security.websocket.AllowedHostsOriginValidatorby default (defense in depth). Missing Origin is still allowed so non-browser clients (curl, testWebsocketCommunicator) continue to work. Upgrade notes: ensuresettings.ALLOWED_HOSTSdoes NOT contain*in production; if you need to opt out for a specific stack, useDjustMiddlewareStack(inner, validate_origin=False)(not recommended). Reported via external penetration test 2026-04-10. (#653) -
Enforce
login_requiredon HTTP GET path — Views withlogin_required = Truerendered full HTML to unauthenticated users on the initial HTTP GET. The WebSocket connection was correctly rejected, but the pre-rendered page content was already visible. Now callscheck_view_auth()beforemount()on HTTP GET and returns 302 toLOGIN_URL. Also callshandle_params()aftermount()on HTTP GET to match the WebSocket path's behavior, preventing state flash on URL-param-dependent views. (#636, fixes #633, #634)
Fixed
-
Prevent
SynchronousOnlyOperationinPerformanceTracker.track_context_size— The tracker calledsys.getsizeof(str(context)), which triggeredQuerySet.__repr__()on any unevaluated querysets in the context dict.__repr__callslist(self[:21]), evaluating the queryset against the database — raisingSynchronousOnlyOperationin the async WebSocket path. Now uses a shallow per-valuegetsizeofsum that does not invoke__repr__/__str__on values, so lazy objects stay lazy. Size estimates are now slightly less precise (don't include recursive inner size) but safe in async contexts. (#651, fixes #649) -
Apply RemoveChild patches before batched InsertChild in same parent group —
applyPatchesinclient.js:1379-1440was filteringInsertChildpatches out of each parent group and applying them viaDocumentFragmentbefore iterating the group for theRemoveChildpatches in that same parent, violating the top-level Remove → Insert phase order. This was latent for keyed content (monotonic dj-ids meant removes still found targets by ID), but fired for<!--dj-if-->placeholder comments — they have no dj-id (only elements get IDs), so their removes fall back to index-based lookup, and by the time the removes ran, the batched inserts had already prepended the new content and shifted indices. The removes then deleted the just-inserted content, leaving empty tab content on multi-tab views (symptom: NYC Claims tab switches showing blank content after the first switch). Fix: split each parent group into non-Insert vs Insert lists, apply all non-Insert patches first in their phase-sorted order, then batch the inserts. (#643, fixes #641, closes #642) -
dj-patchon<a>tags uses href when attribute value is empty — Booleandj-patchon anchor elements (<a href="?tab=docs" dj-patch>) was resolving to the current URL instead of the href destination. Now falls back toel.getAttribute('href')whendj-patchis empty and the element is<a>. (#640) -
Normalize Model instances in
render_full_templatebefore passing to Rust — Django FK fields are class-level descriptors not present in__dict__. Rust'sFromPyObjectextracts__dict__which hasclaimant_id=1(raw FK int) instead of the related object. Now always callsnormalize_django_value()on pre-serialized context so FK relationships are resolved viagetattr()and traversable with dot notation ({{ claim.claimant.first_name }}). (#639) -
Render Django Form/BoundField to SafeString HTML in template context —
{{ form.field_name }}rendered as empty string because the Rust renderer extractedForm.__dict__which doesn't contain computedBoundFieldattributes. Now pre-renders Form and BoundField objects to SafeString HTML viawidget.render()in all four code paths (serialization, template serialization, template rendering, and LiveView state sync). (#631, fixes #621) -
Correct
has_idsattribute name in WebSocket mount response —websocket.pychecked for"data-dj-id="but the Rust renderer emits"dj-id="attributes. This caused_stampDjIds()to be skipped on pre-rendered pages, breaking VDOM patches for large content swaps (e.g. tab switching) while small patches still worked. The SSE path already had the correct check. (#630, fixes #629) -
Sync input
.valuefrom attribute after innerHTML/VDOM patch — When navigating backward in a multi-step wizard, text input values were not visually restored even though the server sent correct VDOM patches.setAttribute('value', x)only updates the HTML attribute (defaultValue), not the.valueDOM property. Now syncs.valuefrom the attribute inpreserveFormValues(), broadcast patches, andmorphElement(). Skips focused inputs, checkboxes, radios, and file inputs. (#625, fixes #624)
[0.4.0] - 2026-03-27
Security
- Fix 25 CodeQL code-scanning alerts in client.js and debug-panel.js — Added UNSAFE_KEYS guard to VDOM SetAttr/RemoveAttr patches (rejects
__proto__,constructor,prototypekeys), replaced direct property assignment withObject.defineProperty()in debug panel state cloning, converted template literal logs to format strings to prevent log injection, and added XSS suppression comments for trusted server-rendered HTML. (#597)
Removed
whitenoisedependency — djust'sASGIStaticFilesHandlerindjust.asgi.get_application()already handles static file serving at the ASGI layer, making WhiteNoise middleware redundant. Removedwhitenoisefrom dependencies, scaffolded projects, and the demo project. Removed system checkC006(daphne without WhiteNoise). (#584)
Added
-
{% dj_flash %}template tag in Rust renderer — RegisteredDjFlashTagHandlerso the flash container renders correctly when templates are processed by the Rust engine. Previously, the tag was only registered as a Django template tag and silently dropped by the Rust renderer. (#590) -
Navigation lifecycle events and CSS class —
djust:navigate-start/djust:navigate-endCustomEvents and.djust-navigatingCSS class on[dj-root]duringdj-navigatetransitions. Enables CSS-only page transitions without monkey-patchingpageLoading. (#585) -
manage.py djust_doctordiagnostic command -- checks Rust extension, Python/Django versions, Channels, Redis, templates, static files, routing, and ASGI server in one command. Supports--json,--quiet,--check NAME, and--verboseflags. -
Enhanced VDOM patch error messages -- patch failures now include patch type,
dj-id, parent element info, and suggested causes (third-party DOM modification,{% if %}block changes). InDEBUG_MODE, a console group with full patch detail is shown. Batch failure summaries include which patch indices failed. -
DEBUG-mode enriched WebSocket errors --
send_errorincludesdebug_detail(unsanitized message),traceback(last 3 frames), andhint(actionable suggestion) whensettings.DEBUG=True.handle_mountlists available LiveView classes when class lookup fails. -
Debug panel warning interceptor -- intercepts
console.warncalls matching[LiveView]prefix and surfaces them as a warning badge on the debug button. Configurable auto-open viaLIVEVIEW_CONFIG.debug_auto_open_on_error. -
Latency simulator in debug panel -- test loading states and optimistic updates with simulated network delay. Presets (Off/50/100/200/500ms), custom value, jitter control, localStorage persistence, and visual badge on the debug button. Latency is injected on both WebSocket send and receive for full round-trip simulation. Only active when
DEBUG_MODE=true. -
Form recovery on reconnect — After WebSocket reconnects, form fields with
dj-changeordj-inputautomatically fire change events to restore server state. Compares DOM values against server-rendered defaults and only fires for fields that differ. Usedj-no-recoverto opt out individual fields. Fields insidedj-auto-recovercontainers are skipped (custom handler takes precedence). Works over both WebSocket and SSE transports. -
Reconnection backoff with jitter — Exponential backoff with random jitter (AWS full-jitter strategy) prevents thundering herd on server restart. Min delay 500ms, max delay 30s, increased from 5 to 10 max attempts. Attempt count shown in reconnection banner (
dj-reconnecting-bannerCSS class) and exposed viadata-dj-reconnect-attemptattribute and--dj-reconnect-attemptCSS custom property on<body>. Banner and attributes cleared on successful reconnect or intentional disconnect. -
page_title/page_metadynamic document metadata — Updatedocument.titleand<meta>tags from any LiveView handler via property setters (self.page_title = "...",self.page_meta = {"description": "..."}). Uses side-channel WebSocket messages (no VDOM diff needed). Supportsog:andtwitter:meta tags with correctpropertyattribute. Works over both WebSocket and SSE transports. -
dj-copyenhancements — Selector-based copy (dj-copy="#code-block"copies the element'stextContent), configurable feedback text (dj-copy-feedback="Done!"), CSS class feedback (dj-copy-classadds a custom class for 2s, defaultdj-copied), and optional server event (dj-copy-event="copied"fires after successful copy for analytics). Backward compatible with existing literal copy behavior. -
dj-auto-recoverattribute for reconnection recovery — After WebSocket reconnects, elements withdj-auto-recover="handler_name"automatically fire a server event with serialized DOM state (form field values anddata-*attributes from the container). Enables the server to restore custom state lost during disconnection. Does not fire on initial page load. Supports multiple independent recovery elements per page. -
dj-debounce/dj-throttleHTML attributes — Apply debounce or throttle to anydj-*event attribute (dj-click,dj-change,dj-input,dj-keydown,dj-keyup) directly in HTML:<button dj-click="search" dj-debounce="300">. Takes precedence overdata-debounce/data-throttle. Supportsdj-debounce="blur"to defer until element loses focus (Phoenix parity).dj-debounce="0"disables default debounce ondj-input. Each element gets its own independent timer. -
Connection state CSS classes —
dj-connectedanddj-disconnectedclasses are automatically applied to<body>based on WebSocket/SSE transport state. Enables CSS-driven UI feedback for connection status (e.g., dimming content, showing offline banners). Both classes are removed on intentional disconnect (TurboNav). Phoenix LiveView'sphx-connected/phx-disconnectedequivalent. -
dj-cloakattribute for FOUC prevention — Elements withdj-cloakare hidden (display: none !important) until the WebSocket/SSE mount response is received, preventing flash of unconnected content. CSS is injected automatically by client.js — no user stylesheet changes needed. Phoenix LiveView'sphx-no-feedbackequivalent. -
Page loading bar for navigation transitions — NProgress-style thin loading bar at the top of the page during TurboNav and
live_redirectnavigation. Always active by default. Exposed aswindow.djust.pageLoadingwithstart(),finish(), andenabledfor manual control. Disable viawindow.djust.pageLoading.enabled = falseor CSS override. -
dj-scroll-into-viewattribute for auto-scroll on render — Elements withdj-scroll-into-vieware automatically scrolled into view after DOM updates (mount, VDOM patch). Supports scroll behavior options:""(smooth/nearest, default),"instant","center","start","end". One-shot per DOM node — uses WeakSet tracking so the same element isn't re-scrolled on every patch, but VDOM-replaced fresh nodes scroll correctly. -
dj-window-*/dj-document-*event scoping — Bind event listeners onwindowordocumentwhile using the declaring element for context extraction (component_id, dj-value-* params). Supportsdj-window-keydown,dj-window-keyup,dj-window-scroll,dj-window-click,dj-window-resize,dj-document-keydown,dj-document-keyup,dj-document-click. Key modifier filtering (e.g.,dj-window-keydown.escape="close_modal") works the same asdj-keydown. Scroll and resize events default to 150ms throttle. Phoenix LiveView'sphx-window-*equivalent, plusdj-document-*as a djust extension. -
dj-click-awayattribute — Fire a server event when the user clicks outside an element:<div dj-click-away="close_dropdown">. Uses capture-phase document listener sostopPropagation()inside the element doesn't prevent detection. Supportsdj-confirmfor confirmation dialogs anddj-value-*params from the declaring element. -
dj-shortcutattribute for declarative keyboard shortcuts — Bind keyboard shortcuts on any element with modifier key support:<div dj-shortcut="ctrl+k:open_search:prevent, escape:close_modal">. Supportsctrl,alt,shift,metamodifiers, comma-separated multiple bindings, andpreventmodifier to suppress browser defaults. Shortcuts are automatically skipped when the user is typing in form inputs (override withdj-shortcut-in-inputattribute). Event params includekey,code, andshortcut(the matched binding string). -
_targetparam in form change/input events — When multiple form fields share onedj-changeordj-inputhandler, the_targetparam now includes the triggering element'sname(orid, ornull), letting the server know which field changed. Fordj-submit, includes the submitter button's name if available. Matches Phoenix LiveView's_targetconvention. -
dj-disable-withattribute for submit buttons — Automatically disable submit buttons during form submission and replace their text with a loading message:<button type="submit" dj-disable-with="Saving...">Save</button>. Prevents double-submit and gives instant visual feedback. Works with bothdj-submitforms anddj-clickbuttons. Original text is restored after server response. -
dj-lockattribute for concurrent event prevention — Disable an element until its event handler response arrives from the server:<button dj-click="save" dj-lock>Save</button>. Prevents rapid double-clicks from triggering duplicate server events. For non-form elements (e.g.,<div>), applies adjust-lockedCSS class instead of thedisabledproperty. All locked elements are unlocked on server response. -
dj-mountedevent for element lifecycle — Fire a server event when an element withdj-mounted="handler_name"enters the DOM after a VDOM patch:<div dj-mounted="on_chart_ready" dj-value-chart-type="bar">. Does not fire on initial page load (only after subsequent patches). Includesdj-value-*params from the mounted element. Uses a WeakSet to prevent duplicate fires for the same DOM node. -
Priority-aware event queue for broadcast and async updates — Server-initiated broadcasts (
server_push) and async completions (_run_async_work) are now tagged withsource="broadcast"andsource="async"respectively, and the client buffers them during pending user event round-trips (same as tick buffering from #560).server_pushnow acquires the render lock and yields to in-progress user events to prevent version interleaving. Client-side pending event tracking upgraded from single ref toSet-based tracking, supporting multiple concurrent pending events. Buffer flushes only when all pending events resolve. -
manage.py djust_gen_live— Model-to-LiveView scaffolding generator — Generate a complete CRUD LiveView scaffold from a model name and field definitions:python manage.py djust_gen_live blog Post title:string body:text. Creates views.py (with@event_handlerCRUD operations), urls.py (usinglive_session()routing), HTML template (withdj-*directives), and tests.py. Supports--dry-run,--force,--no-tests,--api(JSON mode) options. Handles all Django field types including FK relationships. Search usesQobjects for OR logic across text fields. -
on_mounthooks for cross-cutting mount logic — Module-level hooks that run on every LiveView mount, declared via@on_mountdecorator andon_mountclass attribute. Use cases: authentication checks, telemetry, tenant resolution, feature flags. Hooks run after auth checks, beforemount(). Return a redirect URL string to halt the mount pipeline. Hooks are inherited via MRO (parent-first, deduplicated). Includes V009 system check for validation. Phoenixon_mountv0.17+ parity. -
put_flash(level, message)andclear_flash()for ephemeral flash notifications — Phoenixput_flashparity. Queue transient messages (info, success, warning, error) from any event handler; they are flushed to the client over WebSocket/SSE after each response. Includes{% dj_flash %}template tag with auto-dismiss and ARIArole="status"/role="alert"support. (#568) -
handle_paramscalled on initial mount —handle_params(params, uri)is now invoked aftermount()on the initial WebSocket connect, not just on subsequent URL changes. This matches Phoenix LiveView'shandle_params/3contract and eliminates the need to duplicate URL-parsing logic betweenmount()andhandle_params(). Views that don't overridehandle_paramsare unaffected (default is a no-op). -
dj-value-*— Static event parameters — Pass static values alongside events withoutdata-*attributes or hidden inputs:<button dj-click="delete" dj-value-id:int="{{ item.id }}" dj-value-type="soft">. Supports type-hint suffixes (:int,:float,:bool,:json,:list), kebab-to-snake_case conversion, and prototype pollution prevention. Works with all event types:dj-click,dj-submit,dj-change,dj-input,dj-keydown,dj-keyup,dj-blur,dj-focus,dj-poll. Phoenix LiveView'sphx-value-*equivalent.
Fixed
-
True/False/Noneliterals resolved as empty string in custom tag args —get_value()didn't recognize Python boolean/None literals, so{% tag show_labels=False %}producedshow_labels=(empty string) instead ofshow_labels=False. Now handlesTrue/true,False/false, andNone/noneas literal values. (#602) -
Flash and page_metadata not delivered over HTTP POST fallback —
put_flash()andpage_title/page_metaside-channel commands were only flushed over WebSocket. HTTP POST responses now drain_pending_flashand_pending_page_metadataand include them as_flashand_page_metadataarrays in the JSON response. (#590) -
Custom tag args containing lists/objects serialized as
[List]/[Object]—Value::ListandValue::Objectin custom tag arguments were stringified via theDisplaytrait, destroying structured data before it reached Python handlers. Now serialized as JSON viaserde_json. (#589) -
Django filters not applied in custom tag arguments —
{% tag key=var|length %}rendered the literal string instead of the computed value because arg resolution usedcontext.get()(plain lookup) instead ofget_value()(filter-aware). (#591) -
{% if %}inside HTML tag after{{ variable }}emits<!--dj-if-->comment —is_inside_html_tag()only checked the immediately preceding token, missing tag context when{{ variable }}tokens appeared between the tag opening and{% if %}. Addedis_inside_html_tag_at()that scans all preceding tokens. (#580) -
Tick/event version mismatch silently drops user input — Server-initiated ticks could collide with user events, causing VDOM version divergence that silently discarded patches. Added server-side
asyncio.Lockto serialize tick and event render operations, priority yielding so ticks skip during user events, client-side tick patch buffering during pending event round-trips, and monotonic event ref tracking for request/response matching. (#560) -
Focus lost during VDOM patches — When the server pushed VDOM patches (e.g., updating a counter while the user was typing), the focused input/textarea lost focus, cursor position, selection range, and scroll position. Added
saveFocusState()/restoreFocusState()around theapplyPatches()cycle to capture and restoreactiveElement,selectionStart/selectionEnd, andscrollTop/scrollLeft. Element matching uses id → name → dj-id → positional index. Broadcast (remote) updates correctly skip focus restoration. -
VDOM patching fails when
{% if %}blocks add/remove DOM elements — Comment node placeholders (<!--dj-if-->) emitted by the Rust template engine were excluded from client-side child index resolution (getSignificantChildrenandgetNodeByPath), causing path traversal errors and silent patch failures. Also added#commenthandling tocreateNodeFromVNodeso comment placeholders can be correctly created duringInsertChildpatches. (#559)
[0.3.8] - 2026-03-19
Fixed
- Tick auto-refresh causes VDOM version mismatch, silently drops user events —
_run_tickalways calledrender_with_diff()even whenhandle_tick()made no state changes, incrementing the VDOM version on every tick. When a user event interleaved with a tick, the client and server versions diverged, causing all subsequent patches to be silently discarded. Tick now uses_snapshot_assignsto skip render when no public assigns changed. (#560) - WS VDOM cache key collision across tabs — All WebSocket LiveViews shared a single RustLiveView cache slot keyed by
/ws/live/, causing multi-tab sessions to overwrite each other's compiled templates. Cache key now usesrequest.path(the actual page URL) so each view gets its own VDOM baseline. (#561) - Canvas
width/heightcleared duringhtml_updatemorph —morphElementremoved attributes absent from server HTML, resetting canvas 2D contexts and blanking Chart.js charts. Canvaswidthandheightare now preserved during attribute sync. (#561) _force_full_htmlnot checked inhandle_url_change— Views that set_force_full_html = Trueinhandle_params(e.g., when{% for %}loop lengths change) still received VDOM patches instead of full HTML. The flag is now checked afterrender_with_diff()in bothhandle_eventandhandle_url_change. (#559, #561)
Added
dj-patchon selects/inputs uses WSurl_change— Select and input elements withdj-patchnow update via pushState + WebSocketurl_changeinstead of full page reload. A delegateddocumentchange listener survives DOM replacement by morphdom.dj-patch-reloadattribute remains as an opt-in escape hatch for full page navigation. (#561)
[0.3.7] - 2026-03-16
Fixed
- FormMixin: serialization, event handling, and ModelForm support — Fixed 6 issues blocking production use of
FormMixinwithModelFormover WebSocket: added@event_handlertosubmit_form()andvalidate_field(); renamedform_instanceto private_form_instancewith backward-compatible property; storemodel_pk/model_labelas public attributes for re-hydration after WS session restore; syncform_datafrom saved instance afterform_valid(); use FK PK instead of related object; auto-populateform_choiceswith serializable tuples. (#545) dj-hookelements not re-initialized afterhtml_updateorhtml_recovery— When VDOM patches failed and djust fell back to full HTML replacement,updateHooks()was never called, leaving hook elements stale (charts showing old data, canvases empty). AddedupdateHooks()to all DOM replacement paths:html_update,html_recovery, TurboNav reinit, embedded view update, lazy hydration, and streaming updates. (#548)__version__not updated bymake version—make versiononly updatedpyproject.tomlandCargo.tomlbut not the hardcoded__version__in__init__.pyfiles.djust.__version__now stays in sync with the package version. (#547)
Changed
- Extract
reinitAfterDOMUpdate()to DRY up post-DOM-update calls — The repeated pattern ofinitReactCounters()+initTodoItems()+bindLiveViewEvents()+updateHooks()across 10+ call sites is now a single function. New DOM replacement paths only need one call. (#549) - Extract
addEventContext()to consolidate component/embedded view ID extraction — The 8-linegetComponentId/getEmbeddedViewIdpattern appeared 4 times in event binding; now a single helper. (#551) - Extract
isWSConnected()to replace WebSocket state guard chains — TheliveViewWS && liveViewWS.ws && liveViewWS.ws.readyState === WebSocket.OPENpattern appeared across 4 files; now a single predicate. (#552) - Extract
clearOptimisticPending()to consolidate CSS class cleanup — ThequerySelectorAll('.optimistic-pending')removal loop appeared 4 times across 2 files; now a single function. (#553) - Standardize
DJUST_CONFIGaccess viaget_djust_config()— Replaced 10+ inlinegetattr(settings, "DJUST_CONFIG", {})try/except blocks across tenants, PWA, and storage modules with a singleget_djust_config()helper inconfig.py. (#554) - Extract generic
BackendRegistryclass — The duplicated lazy-init / set / reset pattern instate_backends/registry.pyandbackends/registry.pynow delegates to a sharedBackendRegistryclass inutils.py. (#555) - Extract
is_model_list()helper — The repeatedisinstance(value, list) and value and isinstance(value[0], models.Model)check is now a singleis_model_list()function inutils.py, used inmixins/context.pyandmixins/request.py. (#556)
[0.3.6] - 2026-03-14
Breaking Changes
model.idnow returns the native type, not a string —_serialize_model_safely()previously wrappedobj.pkwithstr()when producing the"id"key, causing template comparisons like{% if edit_id == todo.id %}to fail silently whenedit_idwas an integer.model.idnow matchesmodel.pkand returns the native Python type (e.g.int,UUID). Migration: if your templates or event handlers comparemodel.idagainst string literals or string-typed variables, update them to use the native type. PR #262 fixed.pk; this PR (#472) completes the fix for.id.
Fixed
- Skip redundant
mount()on WebSocket connect for pre-rendered pages — When the client sendshas_prerendered=trueon WS connect and saved state exists in the session (written during the HTTP GET), the view's attributes are restored from session instead of re-runningmount(). This eliminates the double page-load cost for views with expensivemount()implementations (e.g. directory scans, API calls). Falls back to callingmount()normally when no saved state is found._ensure_tenant()is now called unconditionally before the restore/mount decision, fixing a regression where multi-tenant views hadself.tenant=Noneon WS connect for pre-rendered pages. (#542) djust cache --allnow correctly clears all sessions on the Redis backend — The CLI calledcleanup_expired(ttl=0)to force-clear sessions, but the semantics ofttl=0changed in 0.3.5 to mean "never expire". The command now calls the explicitdelete_all()method, which uses a Redis pipeline for an efficient single round-trip bulk delete. (#409)dj-paramsattribute no longer silently dropped — Between 0.3.2 and 0.3.6rc2,dj-paramswas removed from the client event-binding code. Templates usingdj-params='{"key": value}'continued to fire click events but the server receivedparams: {}. The attribute is now read and merged into the params object for backward compatibility. Aconsole.warnis emitted in debug mode (globalThis.djustDebug) to notify developers to migrate. (#469)- Prefetch Set not cleared on SPA navigation — The client-side
_prefetchedSet persisted acrosslive_redirectnavigations, preventing links on the new view from being prefetched. Addedclear()towindow.djust._prefetchand call it inhandleLiveRedirect()so each SPA navigation starts with a fresh prefetch state. (#402) - Auto-reload on unrecoverable VDOM state — When VDOM patch recovery fails because recovery HTML is unavailable (e.g. after server restart), the client now auto-reloads the page instead of showing a confusing error overlay. The server sends
recoverable: falseto signal the client. (#421) {% djust_pwa_head %}and other custom tags with quoted arguments containing spaces now render correctly — The Rust template lexer usedsplit_whitespace()to tokenize tag arguments, which broke quoted values likename="My App"into separate tokens (name="MyandApp"). This caused the downstream Python handler to receive malformed arguments, silently returning empty output. Replaced with a quote-aware splitter (split_tag_args) that preserves quoted strings as single arguments. (#419){% load %}tags stripped during template inheritance, breaking inclusion tags — The Rust parser treated{% load %}asNode::Comment, whichnodes_to_template_string()discarded during inheritance reconstruction. When the resolved template was re-parsed, custom tags that relied on Django tag libraries (e.g.{% djust_pwa_head %}) could silently fail. Fixed by adding a dedicatedNode::Loadvariant that preserves library names through reconstruction. Also improved_render_django_tag()error handling: failures now log a full traceback vialogger.exception()and return a visible HTML comment instead of an empty string. (#418)- Checkbox/radio
checkedand<option>selectedstate not updated by VDOM patches —SetAttrandRemoveAttrpatches only calledsetAttribute/removeAttribute, which updates the HTML attribute but not the DOM property. After user interaction the browser separates the two, so server-driven state changes viadj-clickhad no visible effect on checkboxes, radios, or select options. Fixed by syncing the DOM property alongside the attribute. Also fixedcreateNodeFromVNodeto set.checked/.selectedwhen creating new elements. (#422) SESSION_TTL=0breaks all event handling (no DOM patches) —cleanup_expired()methods in bothInMemoryStateBackendandRedisStateBackendnow treatTTL ≤ 0as "never expire". PreviouslySESSION_TTL=0causedcutoff = time.time() - 0, making all sessions appear expired, deleting them immediately, and leaving no state for VDOM patches. (#395)- WebSocket session extraction crashes on Django Channels
LazyObject— Replacedhasattr(scope_session, "session_key")withgetattr(scope_session, "session_key", None)in the consumer's request context builder.hasattr()on a Django ChannelsLazyObjectcan raise non-AttributeErrorexceptions during lazy evaluation, causing the consumer to crash silently. (#396)
Deprecated
-
dj-paramsJSON blob attribute — Use individualdata-*attributes with optional type-coercion suffixes instead.dj-paramswill be removed in a future release.Migration guide (0.3.2 → 0.3.6):
<!-- Before (0.3.2) --> <button dj-click="start_edit" dj-params='{"todo_id": {{ todo.id }}}'>Edit</button> <button dj-click="set_filter" dj-params='{"filter_value": "all"}'>All</button> <!-- After (0.3.6+) --> <button dj-click="start_edit" data-todo-id:int="{{ todo.id }}">Edit</button> <button dj-click="set_filter" data-filter-value="all">All</button>
Type-coercion suffixes:
:int,:float,:bool,:json. Kebab-case attribute names are auto-converted tosnake_casefor server handler parameters.
Added
djust-deployCLI — newpython/djust/deploy_cli.pymodule providing deployment commands for djustlive.com. Available via thedjust-deployentry point after installation. (#437)djust-deploy login— prompts for email/password, authenticates against djustlive.com, and stores the token in~/.djustlive/credentials(mode0o600)djust-deploy logout— calls the server logout endpoint and removes the local credentials filedjust-deploy status [project]— fetches current deployment state; optionally filtered by project slugdjust-deploy deploy <project-slug>— validates the git working tree is clean, triggers a production deployment, and streams build logs to stdout--serverflag /DJUST_SERVERenv var to override the default server URL (https://djustlive.com)
- TypeScript type stubs updated —
DjustStreamOpnow includes"done"and"start"operation types and an optionalmodefield ("append" | "replace" | "prepend").getActiveStreams()return type changed fromMaptoRecord. .flex-betweenCSS utility class — Added to demo project'sutilities.cssfor laying out flex children horizontally with space-between. Use on card headers or any flex container that needs a title on the left and action widget on the right. (#397)- Debug toolbar state size visualization — New "Size Breakdown" table in State tab shows per-variable memory and serialized byte sizes with human-readable formatting (B/KB/MB). Added
_debug_state_sizes()method toPostProcessingMixinincluded in both mount and event debug payloads. (#459) - Debug panel TurboNav persistence — Event, patch, network, and state history now persist across TurboNav navigation via sessionStorage (30s window). Panel state restores on next page if navigated within 30 seconds. (#459)
- TurboNav integration guide — Comprehensive guide covering setup, navigation lifecycle, inline script handling, known caveats, and design decisions:
docs/guides/turbonav-integration.md. (#459) - Debug panel search extended to Network and State tabs — The search bar in the debug panel now filters across all data tabs. The Network tab shows a
N / totalcount label when a query narrows the message list (#530). The State tab filters history entries by trigger, event name, and serialized state content, with the sameN / totalcount label (#520). OverlappingnameFilterandsearchQueryon the Events tab now correctly apply AND semantics (#532). (#541)
[0.3.6rc4] - 2026-03-13
Fixed
- Skip redundant
mount()on WebSocket connect for pre-rendered pages — When the client sendshas_prerendered=trueon WS connect and saved state exists in the session (written during the HTTP GET), the view's attributes are restored from session instead of re-runningmount(). This eliminates the double page-load cost for views with expensivemount()implementations (e.g. directory scans, API calls). Falls back to callingmount()normally when no saved state is found._ensure_tenant()is now called unconditionally before the restore/mount decision, fixing a regression where multi-tenant views hadself.tenant=Noneon WS connect for pre-rendered pages. (#542)
[0.3.6rc3] - 2026-03-13
Breaking Changes
model.idnow returns the native type, not a string —_serialize_model_safely()previously wrappedobj.pkwithstr()when producing the"id"key, causing template comparisons like{% if edit_id == todo.id %}to fail silently whenedit_idwas an integer.model.idnow matchesmodel.pkand returns the native Python type (e.g.int,UUID). Migration: if your templates or event handlers comparemodel.idagainst string literals or string-typed variables, update them to use the native type. PR #262 fixed.pk; this PR (#472) completes the fix for.id.
Fixed
djust cache --allnow correctly clears all sessions on the Redis backend — The CLI calledcleanup_expired(ttl=0)to force-clear sessions, but the semantics ofttl=0changed in 0.3.5 to mean "never expire". The command now calls the explicitdelete_all()method, which uses a Redis pipeline for an efficient single round-trip bulk delete. (#409)dj-paramsattribute no longer silently dropped — Between 0.3.2 and 0.3.6rc2,dj-paramswas removed from the client event-binding code. Templates usingdj-params='{"key": value}'continued to fire click events but the server receivedparams: {}. The attribute is now read and merged into the params object for backward compatibility. Aconsole.warnis emitted in debug mode (globalThis.djustDebug) to notify developers to migrate. (#469)- Prefetch Set not cleared on SPA navigation — The client-side
_prefetchedSet persisted acrosslive_redirectnavigations, preventing links on the new view from being prefetched. Addedclear()towindow.djust._prefetchand call it inhandleLiveRedirect()so each SPA navigation starts with a fresh prefetch state. (#402) - Auto-reload on unrecoverable VDOM state — When VDOM patch recovery fails because recovery HTML is unavailable (e.g. after server restart), the client now auto-reloads the page instead of showing a confusing error overlay. The server sends
recoverable: falseto signal the client. (#421) {% djust_pwa_head %}and other custom tags with quoted arguments containing spaces now render correctly — The Rust template lexer usedsplit_whitespace()to tokenize tag arguments, which broke quoted values likename="My App"into separate tokens (name="MyandApp"). This caused the downstream Python handler to receive malformed arguments, silently returning empty output. Replaced with a quote-aware splitter (split_tag_args) that preserves quoted strings as single arguments. (#419){% load %}tags stripped during template inheritance, breaking inclusion tags — The Rust parser treated{% load %}asNode::Comment, whichnodes_to_template_string()discarded during inheritance reconstruction. When the resolved template was re-parsed, custom tags that relied on Django tag libraries (e.g.{% djust_pwa_head %}) could silently fail. Fixed by adding a dedicatedNode::Loadvariant that preserves library names through reconstruction. Also improved_render_django_tag()error handling: failures now log a full traceback vialogger.exception()and return a visible HTML comment instead of an empty string. (#418)- Checkbox/radio
checkedand<option>selectedstate not updated by VDOM patches —SetAttrandRemoveAttrpatches only calledsetAttribute/removeAttribute, which updates the HTML attribute but not the DOM property. After user interaction the browser separates the two, so server-driven state changes viadj-clickhad no visible effect on checkboxes, radios, or select options. Fixed by syncing the DOM property alongside the attribute. Also fixedcreateNodeFromVNodeto set.checked/.selectedwhen creating new elements. (#422) SESSION_TTL=0breaks all event handling (no DOM patches) —cleanup_expired()methods in bothInMemoryStateBackendandRedisStateBackendnow treatTTL ≤ 0as "never expire". PreviouslySESSION_TTL=0causedcutoff = time.time() - 0, making all sessions appear expired, deleting them immediately, and leaving no state for VDOM patches. (#395)- WebSocket session extraction crashes on Django Channels
LazyObject— Replacedhasattr(scope_session, "session_key")withgetattr(scope_session, "session_key", None)in the consumer's request context builder.hasattr()on a Django ChannelsLazyObjectcan raise non-AttributeErrorexceptions during lazy evaluation, causing the consumer to crash silently. (#396)
Deprecated
-
dj-paramsJSON blob attribute — Use individualdata-*attributes with optional type-coercion suffixes instead.dj-paramswill be removed in a future release.Migration guide (0.3.2 → 0.3.6):
<!-- Before (0.3.2) --> <button dj-click="start_edit" dj-params='{"todo_id": {{ todo.id }}}'>Edit</button> <button dj-click="set_filter" dj-params='{"filter_value": "all"}'>All</button> <!-- After (0.3.6+) --> <button dj-click="start_edit" data-todo-id:int="{{ todo.id }}">Edit</button> <button dj-click="set_filter" data-filter-value="all">All</button>
Type-coercion suffixes:
:int,:float,:bool,:json. Kebab-case attribute names are auto-converted tosnake_casefor server handler parameters.
Added
djust-deployCLI — newpython/djust/deploy_cli.pymodule providing deployment commands for djustlive.com. Available via thedjust-deployentry point after installation. (#437)djust-deploy login— prompts for email/password, authenticates against djustlive.com, and stores the token in~/.djustlive/credentials(mode0o600)djust-deploy logout— calls the server logout endpoint and removes the local credentials filedjust-deploy status [project]— fetches current deployment state; optionally filtered by project slugdjust-deploy deploy <project-slug>— validates the git working tree is clean, triggers a production deployment, and streams build logs to stdout--serverflag /DJUST_SERVERenv var to override the default server URL (https://djustlive.com)
- TypeScript type stubs updated —
DjustStreamOpnow includes"done"and"start"operation types and an optionalmodefield ("append" | "replace" | "prepend").getActiveStreams()return type changed fromMaptoRecord. .flex-betweenCSS utility class — Added to demo project'sutilities.cssfor laying out flex children horizontally with space-between. Use on card headers or any flex container that needs a title on the left and action widget on the right. (#397)- Debug toolbar state size visualization — New "Size Breakdown" table in State tab shows per-variable memory and serialized byte sizes with human-readable formatting (B/KB/MB). Added
_debug_state_sizes()method toPostProcessingMixinincluded in both mount and event debug payloads. (#459) - Debug panel TurboNav persistence — Event, patch, network, and state history now persist across TurboNav navigation via sessionStorage (30s window). Panel state restores on next page if navigated within 30 seconds. (#459)
- TurboNav integration guide — Comprehensive guide covering setup, navigation lifecycle, inline script handling, known caveats, and design decisions:
docs/guides/turbonav-integration.md. (#459) - Debug panel search extended to Network and State tabs — The search bar in the debug panel now filters across all data tabs. The Network tab shows a
N / totalcount label when a query narrows the message list (#530). The State tab filters history entries by trigger, event name, and serialized state content, with the sameN / totalcount label (#520). OverlappingnameFilterandsearchQueryon the Events tab now correctly apply AND semantics (#532). (#541)
[0.3.5] - 2026-03-05
Added
djust-deployCLI — newpython/djust/deploy_cli.pymodule providing deployment commands for djustlive.com. Install withpip install djust[deploy]. Available via thedjust-deployentry point:djust-deploy login— prompts for email/password, authenticates against djustlive.com, and stores the token in~/.djustlive/credentials(mode0o600)djust-deploy logout— calls the server logout endpoint and removes the local credentials filedjust-deploy status [project]— fetches current deployment state; optionally filtered by project slugdjust-deploy deploy <project-slug>— validates the git working tree is clean, triggers a production deployment, and streams build logs to stdout
Fixed
dj-hookelements now initialize afterdj-navigatenavigation —updateHooks()is called afterlive_redirect_mountreplaces DOM content via WebSocket and SSE mount handlers. Previously, hook lifecycle callbacks (mounted(),destroyed()) were skipped after client-side navigation, leaving hook-dependent elements (e.g., Chart.js canvases) uninitialized. (#408)- Event handler exceptions now logged with full traceback in production — Previously,
handle_exception()only logged the exception class name (e.g.ValueError) whenDEBUG=False, hiding the error message and stack trace. Now logs type, message, and traceback atERRORlevel regardless ofDEBUGmode. Client responses remain generic in production. (#415) - DJE-053 no longer fires as a warning for idempotent event handlers — When an
@event_handlerruns successfully but produces no DOM changes (e.g. toggle clicked in target state, debounced input with unchanged results, side-effect-only handlers), the empty diff is now silently dropped atDEBUGlevel rather than logged as aWARNING. This matches Phoenix LiveView behaviour. TheWARNING-level DJE-053 is preserved for genuine VDOM failures (patches=None), which fall back to a full HTML update and risk losing event listeners. (#415)
[0.3.5rc2] - 2026-03-04
Fixed
- VDOM patching with conditional
{% if %}blocks —InsertChildandRemoveChildpatches now includeref_dandchild_dfields for ID-based DOM resolution, preventing stale-index mis-targeting when{% if %}blocks add or remove elements that shift sibling positions. Falls back to index-based resolution for backwards compatibility. (#410)
[0.3.5rc1] - 2026-02-26
Added
- Type stubs for Rust-injected LiveView methods —
.pyistubs forlive_redirect,live_patch,push_event,stream, and related methods so mypy/pyright catch typos at lint time. (#390) - Navigation Patterns guide — Documents when to use
dj-navigatevslive_redirectvslive_patch. (#390) - Testing guide — Django testing best practices and pytest setup for djust applications. (#390)
- System checks reference — New
docs/system-checks.mdcovering all 37 check IDs (C/V/S/T/Q) with severity, detection method, suppression patterns, and known false positives. (#398)
Security
mark_safe(f"...")eliminated in core framework —components/base.pynow usesformat_html()to avoid XSS risk in component rendering. (#390)- Exception details no longer exposed in production —
render_template()previously returnedf"<div>Error: {e}</div>"unconditionally, leaking internal Rust template engine details. Now returns a generic message in production; error details are only shown whensettings.DEBUG = True. (#385) - Playground XSS fixed — Replaced
innerHTMLassignment with a sandboxed iframe for user-editable preview content. (#384) - Prototype pollution guard — Added safeguards against prototype pollution in client-side JS. (#384)
Fixed
{% if %}inside attribute values no longer shifts VDOM path indices — Conditional attribute fragments were causing off-by-one errors in VDOM diffing. (#390)super().__init__()added to component and backend subclasses —TenantAwareRedisBackend,TenantAwareMemoryBackend, and several example components were missingsuper().__init__()calls, causing MRO issues. (#386)- Unused
escapeimport removed fromdata_table.py— CodeQL alert resolved. (#387) render_full_templatesignature mismatch fixed —no_template_demo.pyoverride now correctly acceptsserialized_context. (#387)- V004 false positives on lifecycle methods —
handle_params(),handle_disconnect(),handle_connect(), andhandle_event()no longer incorrectly trigger the V004 system check. (#398) - T013 false positives for
{{ view_path }}—dj-view="{{ view_path }}"(Django template variable injection) is now correctly recognised as valid by T013. (#398) - V008 false positives for
-> str-annotated functions — Functions with primitive return-type annotations (e.g.-> str,-> int) no longer trigger V008 when their result is assigned inmount(). (#398) - Test isolation —
test_checks.pyanddouble_bind.test.jsno longer fail when run as part of the full suite. (#390)
[0.3.4] - 2026-02-24
Stable release — promotes 0.3.3rc1 through 0.3.3rc3. All changes below were present in the RC series; this entry summarises them for the stable changelog.
Added
- 6 new Django template tags in Rust renderer —
{% widthratio %},{% firstof %},{% templatetag %},{% spaceless %},{% cycle %},{% now %}. (#329) - System checks
djust.T011/T012/T013— Warns at startup for unsupported Rust template tags, missingdj-view, and invaliddj-viewpaths. (#293, #329) - Deployment guides — Railway, Render, and Fly.io. (#247)
- Navigation and LiveView invariants documentation. (#304, #316)
Fixed
- #380:
{% if %}in HTML attribute values no longer emits<!--dj-if-->comment — Produced malformed HTML (e.g.class="btn <!--dj-if-->"). Empty string is emitted instead; text-node VDOM anchor is unaffected. (#381) - #382:
{% elif %}chains in attribute values propagatein_tag_context— All elif nodes in a chain now inherit the outer{% if %}'s attribute context. (#383) {% if/else %}branches miscounting div depth in template extraction. (#365)- VDOM extraction used fully-merged
{% extends %}document. (#366) TypeError: Illegal invocationin debug panel on Chrome/Edge. (#367)dj-patch('/')now correctly updates browser URL to root path. (#307)live_patchrouting restored —handleNavigationdispatch now fires correctly. (#307)- T003 false positives eliminated —
{% include %}check now examines the include path, not whole-file content. (#331)
[0.3.3rc3] - 2026-02-24
Fixed
- #382:
{% elif %}inside HTML attribute values propagatesin_tag_context— When{% if a %}...{% elif b %}...{% endif %}appears inside an attribute value and all conditions are false, the elif node previously emitted<!--dj-if-->(malformed HTML). Fixed by threadingin_tag_contextas a parameter intoparse_if_block()so elif nodes inherit the outer if's attribute context. (#382)
[0.3.3rc2] - 2026-02-24
Fixed
{% if/else %}branches miscounting div depth in template extraction —_extract_liveview_root_with_wrapperand the other extraction methods treated both branches of a{% if/else %}block as independent div opens, causing depth to never reach 0 when both branches opened a div sharing a single closing</div>. This caused the entire template to be returned as root, making the view non-reactive. Fixed with a shared_find_closing_div_pos()static method that uses a branch stack to restore depth at{% else %}/{% elif %}tags, so mutually-exclusive branches are counted as one open. (#365)- VDOM extraction used fully-merged
{% extends %}document — For inherited templates,get_template()extracted the VDOM root from the fully-resolved document (base HTML + inlined blocks), which contains surrounding HTML that the depth counter could trip over. Now prefers the child template source when it containsdj-root/dj-view, which holds exactly the block content needed. Also fixes the exception fallback path: the raw child source (containing{% extends %}) was incorrectly stored in_full_template, causingrender_full_templateto attempt rendering a non-standalone template. (#366) TypeError: Illegal invocationin debug panel on Chrome/Edge —_hookExistingWebSocketcalled native WebSocket getter/setter functions viaFunction.prototype.call()from external code, which fails V8's brand check on IDL-generated bindings. Fixed by using normal property access (ws.onmessage) and assignment (ws.onmessage = handler) instead ofdesc.get/set.call(ws). (#367)
[0.3.3rc1] - 2026-02-21
Added
- 6 new Django template tags in Rust renderer — Implemented
{% widthratio %},{% firstof %},{% templatetag %},{% spaceless %},{% cycle %}, and{% now %}in the Rust template engine. These tags were previously rendered as HTML comments with warnings. (#329) - System check
djust.T011for unsupported template tags — Warns at startup when templates use Django tags not yet implemented in the Rust renderer (ifchanged,regroup,resetcycle,lorem,debug,filter,autoescape). Suppressible with{# noqa: T011 #}. (#329) - System check
djust.T012for missingdj-view— Detects templates that usedj-*event directives without adj-viewattribute, which would silently fail at runtime. (#293) - System check
djust.T013for invaliddj-viewpaths — Detects empty or malformeddj-viewattribute values. (#293) {% now %}supports 35+ Django date format specifiers — IncludingS(ordinal suffix),t(days in month),w/W(weekday/week number),L(leap year),c(ISO 8601),r(RFC 2822),U(Unix timestamp), and Django's specialPformat (noon/midnight).- Deployment guides — Added deployment documentation for Railway, Render, and Fly.io. (#247)
- Navigation best practices documentation — Documented
dj-patchvsdj-clickfor client-side navigation, withhandle_params()patterns. (#304) - LiveView invariants documentation — Documented root container requirement and
**kwargsconvention for event handlers. (#316)
Fixed
- #380:
{% if %}inside HTML attribute values no longer emits<!--dj-if-->comment — When a{% if %}block with no else branch evaluates to false inside an HTML attribute value (e.g.class="btn {% if active %}active{% endif %}"), the Rust renderer now emits an empty string instead of the<!--dj-if-->VDOM placeholder. The placeholder is only meaningful as a DOM child node; inside an attribute it produced malformed HTML (e.g.class="btn <!--dj-if-->"). Text-node context is unaffected — the anchor comment is still emitted there for VDOM stability (fix for DJE-053 / #295). - False
{% if %}blocks now emit<!--dj-if-->placeholder instead of empty string — Gives the VDOM diffing engine a stable DOM anchor to target when the condition later becomes true, resolving DJE-053 / issue #295. dj-patch('/')now correctly updates the browser URL to the root path — Removed theurl.pathname !== '/'guard inbindNavigationDirectivesthat prevented the browser URL from being updated when patching to/. The guard was silently ignoring root-path patches. (#307)live_patchrouting restored —handleNavigationdispatch now fires correctly — Fixed dict merge order in_flush_navigationsotype: 'navigation'is no longer overwritten by**cmd. Added anactionfield to carry the nav sub-type (live_patch/live_redirect);handleNavigationnow dispatches ondata.actioninstead ofdata.type. Previously the clientswitch case 'navigation':never matched becausetypewas being overwritten with"live_patch". Note:data.action || data.typefallback is kept for old JS clients that send messages without anactionfield — this fallback is planned for removal in the next minor release. (#307)- T003 false positives eliminated — The
{% include %}check now examines the include path instead of the whole file content, preventing false warnings on templates that include SVGs or modals alongsidedj-*directives. (#331)
[0.3.2] - 2026-02-18
Added
- TypeScript definitions (
djust.d.ts) — Comprehensive ambient TypeScript declaration file shipped with the Python package atstatic/djust/djust.d.ts. Covers:window.djustnamespace,LiveViewWebSocketandLiveViewSSEtransport classes,DjustHooklifecycle interface (mounted,beforeUpdate,updated,destroyed,disconnected,reconnected),DjustHookContext(this.el,this.pushEvent,this.handleEvent),dj-modelbinding types, streaming API types (DjustStreamMessage,DjustStreamOp), upload progress event types (DjustUploadEntry,DjustUploadConfig,DjustUploadProgressEventDetail), and thedjust:upload:progresscustom DOM event. Use via/// <reference path="..." />or add totsconfig.json. - Python type stubs (
_rust.pyi) — PEP 561 compliant type stubs for the PyO3 Rust extension module (djust._rust). Covers all exported functions (render_template,render_template_with_dirs,diff_html,resolve_template_inheritance,fast_json_dumps, serialization helpers, tag handler registry) and classes (RustLiveView,SessionActorHandle,SupervisorStatsPy, and all 15 Rust UI components). Enables full IDE autocomplete and mypy type checking for the Rust extension. - SSE (Server-Sent Events) fallback transport — djust now automatically falls back to SSE when WebSocket is unavailable (corporate proxies, enterprise firewalls). Architecture:
EventSourcefor server→client push, HTTP POST for client→server events. Transport negotiation is automatic: WebSocket is tried first; SSE activates after all reconnect attempts fail. Register the endpoint withpath("djust/", include(djust.sse.sse_urlpatterns))and include03b-sse.jsin your template. Feature limitations: no binary file uploads, no presence tracking, no actor-based state. Seedocs/sse-transport.mdfor full setup guide. - Type stub files (.pyi) for LiveView and mixins — Added PEP 561 compliant type stubs for
NavigationMixin,PushEventMixin,StreamsMixin,StreamingMixin, andLiveViewto enable IDE autocomplete and mypy type checking for runtime-injected methods likelive_redirect,live_patch,push_event,stream,stream_insert,stream_delete, andstream_to. Includespy.typedmarker file and comprehensive test suite. @backgrounddecorator for async event handlers — New decorator that automatically runs the entire event handler in a background thread viastart_async(). Simplifies syntax for long-running operations (AI generation, API calls, file processing) without needing explicit callback splitting. Can be combined with other decorators like@debounce. Task name is automatically set to the handler's function name for cancellation tracking. (#313)start_async()keeps loading state active during background work — WebSocket responses includeasync_pendingflag when astart_async()callback is running, preventing loading spinners from disappearing prematurely. Async completion responses includeevent_nameso the client clears the correct loading state. Supports named tasks for tracking and cancellation viacancel_async(name). Optionalhandle_async_result(name, result, error)callback for completion/error handling. (#313, #314)dj-loading.forattribute — Scope anydj-loading.*directive to a specific event name, regardless of DOM position. Allows spinners, disabled buttons, and other loading indicators anywhere in the page to react to a named event. (#314)AsyncWorkMixinincluded inLiveViewbase class —start_async()is now available on all LiveViews without explicit mixin import. (#314)- Loading state re-scan after DOM patches —
scanAndRegister()is called after everybindLiveViewEvents()so dynamically rendered elements (e.g., inside modals) get loading state registration. Stale entries for disconnected elements are cleaned up automatically. (#314) - System check
djust.T010for dj-click navigation antipattern — Detects elements usingdj-clickwith navigation-related data attributes (data-view,data-tab,data-page,data-section). This pattern should usedj-patchinstead for proper URL updates, browser history support, and bookmarkable views. Warning severity. (#305) - System check
djust.Q010for navigation state in event handlers — Heuristic INFO-level check that detects@event_handlermethods setting navigation state variables (self.active_view,self.current_tab, etc.) without usingpatch()orhandle_params(). Suggests converting todj-patchpattern for URL updates and back-button support. Can be suppressed with# noqa: Q010. (#305) - Type stubs for Rust extension and LiveView — Added
.pyitype stub files for_rustmodule andLiveViewclass, enabling IDE autocomplete, mypy/pyright type checking, and catching typos likelive_navigate(should belive_patch) at lint time. Includespy.typedmarker for PEP 561 compliance and comprehensive documentation indocs/TYPE_STUBS.md.
Deprecated
data.typefallback inhandleNavigation— Thedata.action || data.typefallback for pre-#307 clients (added for backwards compatibility in #318) will be removed in the next minor release. Server now sendsdata.actionon all navigation messages. Update any custom client code that sends navigation messages without anactionfield.
Fixed
- Silent
str()coercion for non-serializable LiveView state — Non-serializable objects stored inself.*duringmount()(e.g., service instances, API clients) were silently converted to strings, causing confusingAttributeErroron subsequent requests far from the root cause.normalize_django_value()now logs a warning before falling back with the type name, module, and guidance on how to fix. Opt-in strict mode (DJUST_STRICT_SERIALIZATION = True) raisesTypeErrorinstead of coercing, recommended for development. New static checkdjust.V008(AST-based) detects non-primitive assignments inmount()at development time. (#292) - System check S005 incorrectly warns on views with
login_required = False— The S005 security check now correctly distinguishes between intentionally public views (login_required = False) and views that haven't addressed authentication at all (login_required = None). Previously, views withlogin_required = Falsewere incorrectly flagged as missing authentication due to a truthy test. The check now uses explicitis not Nonecomparisons to distinguish intentional public access from unaddressed auth. (#303) |safefilter rendering empty string for nested SafeString values — When mark_safe() HTML was stored in lists of dicts or nested dicts, the |safe filter rendered an empty string instead of preserving the HTML. The _collect_safe_keys() function now recursively scans nested dicts and lists using dotted path notation (e.g., "items.0.content") to track all SafeString locations. Includes circular reference protection to prevent RecursionError on tree/graph structures. (#317)- VDOM diff incorrectly matching siblings when
{% if %}removes nodes — When{% if %}blocks evaluated to false and removed elements, siblings shifted left, causingdiff_indexed_children()to incorrectly match unrelated nodes and generate wrong patches. The template engine now emits<!--dj-if-->placeholder comments when conditions are false (matching Phoenix LiveView's approach), maintaining consistent sibling positions. The VDOM diff detects placeholder-to-content transitions and generatesRemoveChild+InsertChildpatches instead ofReplacepatches for semantic consistency. Eliminates DJE-053 fallback to full HTML updates and removes need forstyle='display:none'workarounds. (#295) - Event listener leak causing duplicate WebSocket sends — Single user actions were triggering the same event multiple times (e.g.
select_project5×,mount3×) because listeners accumulated across VDOM patch/morph cycles without cleanup. Fixed four root causes: (1)initReactCountersnow uses aWeakSetguard to skip already-initialized containers; (2)createNodeFromVNodeno longer pre-marks elements as bound beforebindLiveViewEvents()runs, eliminating a race where newly inserted elements were silently skipped; (3)dj-clickhandlers now read the attribute at fire-time rather than bind-time, somorphElementattribute updates take effect immediately; (4) three unguardedconsole.logcalls in12-vdom-patch.jsare now wrapped inif (globalThis.djustDebug). The existingWeakMap-based deduplication inbindLiveViewEvents()(introduced in #312) correctly prevents re-binding when called repeatedly. (#315) dj-patch('/')failed to update URL andlive_patchrouting broken — Removedurl.pathname !== '/'guard inbindNavigationDirectivesso root-path navigation works. Fixed dict merge order in_flush_navigationso server sendstype='navigation'instead oftype='live_patch'. UpdatedhandleNavigationto dispatch viadata.actionwithdata.action || data.typefallback for backwards compatibility. (#318)- 52 unguarded
console.logcalls in client JS — Allconsole.logcalls across 12 files instatic/djust/src/(excluding the intentional debug panel insrc/debug/) are now wrapped withif (globalThis.djustDebug). Bare logging in production code leaks internal state to browser consoles and violates thedjust.Q003system check. Files affected:00-namespace.js,02-response-handler.js,03-websocket.js,04-cache.js,05-state-bus.js,06-draft-manager.js,07-form-data.js,09-event-binding.js,10-loading-states.js,11-event-handler.js,12-vdom-patch.js,13-lazy-hydration.js. - dj-submit forms sent empty params when created by VDOM patches —
createNodeFromVNodenow correctly collectsFormDatafor submit events; replaceddata-liveview-*-boundattribute tracking withWeakMapto prevent stale binding flags after DOM replacement (#312)
Security
- F-strings in logging calls — Converted 9 logger calls to use %-style formatting (
logger.error("msg %s", val)) instead of f-strings (logger.error(f"msg {val}")). F-strings defeat lazy evaluation, causing string interpolation before the log level check, potentially exposing sensitive data and wasting CPU. Affected files:mixins/template.py,security/__init__.py,security/error_handling.py,template_tags/__init__.py,template_tags/static.py,template_tags/url.py.
Tests
- Regression tests for
|safefilter with nested dicts — Added comprehensive tests verifying that|safefilter works correctly for HTML content in nested dict/list values, preventing issue #317 from recurring
[0.3.2rc1] - 2026-02-15
Fixed
- Form data lost on
dj-submit— Client-only properties (_targetElement,_optimisticUpdateId,_skipLoading,_djTargetSelector) are now stripped from event params before serialization. Previously,HTMLFormElementreferences in params corrupted the JSON payload, overwriting form field data with the element's indexed children. (#308) @change→dj-changein form adapters — All three framework adapters (Bootstrap 5, Tailwind, Plain) rendered@change="validate_field"instead ofdj-change="validate_field", causing real-time field validation to silently fail. (#310)EmailFieldrendered astype="text"—_get_field_type()checkedCharFieldbeforeEmailField(which inherits fromCharField), so email fields never gottype="email". Reordered the isinstance checks. (#310)
Security
- XSS in
FormMixin.render_field()— Removedrender_field(),_render_field_widget(), and_attrs_to_string()fromFormMixin. These methods used f-strings with no escaping to build HTML, allowing stored XSS via form field values. Useas_live()/as_live_field()(which delegate to framework adapters with properescape()) instead. (#310) - Textarea content not escaped in adapters —
_render_input()passed raw textarea values to_build_tag()content withoutescape(). Addedescape(str(value))for textarea content. (#310)
Changed
- Framework adapters deduplicated — Created
BaseAdapterwith all shared rendering logic.Bootstrap5Adapter,TailwindAdapter, andPlainAdapterreduced from ~200 lines each to ~10 lines of class attributes.frameworks.pyreduced from ~657 to ~349 lines. (#310) _model_instancesupport for ModelForm editing —FormMixin.mount()now reads field values from_model_instanceif set and the form is aModelForm._create_form()passesinstance=to the form constructor. (#310)
Deprecated
LiveViewForm— EmitsDeprecationWarningon subclass. Adds no functionality overdjango.forms.Form. Will be removed in 0.4. (#310)
Removed
FormMixin.render_field()— Insecure (XSS via f-strings) and duplicated adapter logic. Useas_live_field()instead. (#310)form_field()function — Dead code, never called. Removed fromforms.pyand__all__. (#310)
[0.3.1] - 2026-02-14
Changed
- 3.8x faster rendering for large pages — Optimized
get_context_data()by replacingdir(self)iteration (~300 inherited Django View attributes, ~50ms) with targeted__dict__+ MRO walk (<1ms). Addeddj-update="ignore"optimization to Rust VDOM diff engine, skipping subtrees the client won't patch (240ms → 17ms). Combined with template-level optimizations, reduces event roundtrip from ~160ms to ~42ms on pages with large static content.
0.3.0 - 2026-02-14
Added
-
dj-confirmattribute — Declarative confirmation dialogs for event handlers. Adddj-confirm="Are you sure?"to anydj-clickelement to show a browser confirmation dialog before dispatching the event. (#302) -
CSS Framework Support — Comprehensive Tailwind CSS integration with three-part system: (1) System checks (
djust.C010,djust.C011,djust.C012) automatically warn about Tailwind CDN in production, missing compiled CSS, and manualclient.jsloading. (2) Graceful fallback auto-injects Tailwind CDN in development mode whenoutput.cssis missing. (3) CLI helper commandpython manage.py djust_setup_css tailwindcreatesinput.csswith Tailwind v4 syntax, auto-detects template directories, finds Tailwind CLI, and builds CSS with optional--watchand--minifyflags. Eliminates duplicate client.js race conditions and guides developers toward production-ready setup.
Fixed
- Server-side template processing now auto-infers dj-root from dj-view — All template extraction methods (
_extract_liveview_content,_extract_liveview_root_with_wrapper,_extract_liveview_template_content,_strip_liveview_root_in_html) now fall back to[dj-view]when[dj-root]is not present, matching the client-sideautoStampRootAttributes()behavior introduced in PR #297. This fixes a bug where templates with onlydj-view(no explicitdj-root) would fail to render correctly. (#300) - Client-side autoMount now correctly reads dj-view attribute — Fixed
autoMount()to usegetAttribute('dj-view')instead ofcontainer.dataset.djView. ThedatasetAPI readsdata-*attributes, butdj-viewis not a data attribute, causing the attribute to be missed. (#300) - System check T002 downgraded from WARNING to INFO — Since
dj-rootis now optional and auto-inferred fromdj-view(per PR #297), the T002 check is now informational rather than a warning. The message now clarifies that auto-inference is working correctly. (#300) - Duplicate client.js loading race condition — djust now automatically detects and warns (via
djust.C012system check) when base or layout templates manually include<script src="{% static 'djust/client.js' %}">. Since the framework auto-injectsclient.js, manual loading causes double-initialization and console warnings. The check provides clear guidance to remove manual script tags. - Tailwind CDN in production — New
djust.C010system check warns when Tailwind CDN (cdn.tailwindcss.com) is detected in production templates (DEBUG=False). Provides actionable guidance to compile CSS withdjust_setup_csscommand or Tailwind CLI. Prevents slow CDN performance and console warnings in production.
Security
- Pre-Release Security Audit Process — Comprehensive security infrastructure to prevent vulnerabilities like the mount handler RCE (Issue #298) from reaching production. Includes 259 new security tests (Python + Rust) covering parameter injection, file upload attacks, URL injection, and XSS prevention across all contexts. Three GitHub workflows provide automated security scanning (bandit, safety, cargo-audit, npm audit, CodeQL), hot spot detection (auto-labels PRs touching security-sensitive code), and CI security test job requiring 85% coverage for security-sensitive modules. New pre-release security audit template with 7-phase checklist ensures comprehensive review before each release. Documentation updates establish mandatory security gates and review requirements for changes to hot spot files.
Dependencies
[0.3.0rc5] - 2026-02-11
Added
- Automatic change tracking — Phoenix-style render optimization. The framework automatically detects which context values changed between renders and only sends those to Rust's
update_state(). Replaces the manualstatic_assignsAPI. Two-layer detection: snapshot comparison for instance attributes,id()reference comparison for computed values (e.g.,@lru_cacheresults). Immutable types (str,int,float,bool,None,bytes,tuple,frozenset) skipdeepcopyin snapshots.
Removed
static_assignsclass attribute — Replaced by automatic change tracking. The framework now detects unchanged values automatically — no manual annotation needed.
[0.3.0rc4] - 2026-02-11
Added
- All 57 Django template filters — The Rust template engine now supports the complete set of Django built-in filters. Added 24 filters across two batches:
default_if_none,wordcount,wordwrap,striptags,addslashes,ljust,rjust,center,make_list,json_script,force_escape,escapejs,linenumbers,get_digit,iriencode,urlize,urlizetrunc,truncatechars_html,truncatewords_html,safeseq,escapeseq,unordered_list,phone2numeric,pprint. (#246, #254) - Authentication & Authorization — Opinionated, framework-enforced auth for LiveViews. View-level
login_requiredandpermission_requiredclass attributes (plusLoginRequiredMixin/PermissionRequiredMixinfor Django-familiar patterns). Custom auth logic viacheck_permissions()hook. Handler-level@permission_required()decorator for protecting individual event handlers. Auth checks run server-side beforemount()and before handler dispatch — no client-side bypass possible. Integrates withdjust_auditcommand (shows auth posture per view) and Django system checks (djust.S005warns on unprotected views with exposed state). - Navigation & URL State —
live_patch()updates URL query params without remount,live_redirect()navigates to a different view over the same WebSocket. Includeshandle_params()callback,live_session()URL routing helper, and client-sidedj-patch/dj-navigatedirectives with popstate handling. (#236) - Presence Tracking — Real-time user presence with
PresenceMixinandPresenceManager. Pluggable backends (in-memory and Redis). IncludesLiveCursorMixinandCursorTrackerfor collaborative live cursor features. (#236) - Streaming —
StreamingMixinfor real-time partial DOM updates (e.g., LLM token-by-token streaming). Providesstream_to(),stream_insert(),stream_text(),stream_error(),stream_start()/stream_done(), andpush_state(). Batched at ~60fps to prevent flooding. (#236) - File Uploads —
UploadMixinwith binary WebSocket frame protocol for chunked file uploads. Includes progress tracking, magic bytes validation, file size/extension/MIME checking, and client-sidedj-upload/dj-upload-dropdirectives. (#236) - JS Hooks —
dj-hookattribute for client-side JavaScript lifecycle hooks (mounted, updated, destroyed, disconnected, reconnected). (#236) - Model Binding —
dj-modeltwo-way data binding with.lazyand.debounce-Nmodifiers. Server-sideModelBindingMixinwith security field blocklist and type coercion. (#236) - Client Directives —
dj-confirmconfirmation dialogs,dj-targetscoped updates, embedded view routing in event handlers. (#236) - Server-Push API — Background tasks (Celery, management commands, cron jobs) can now push state updates to connected LiveView clients via
push_to_view(). Includes per-view channel groups (auto-joined on mount), a sync/async public API (push_to_view/apush_to_view), and periodichandle_tick()for self-updating views. (#230) - Progressive Web App (PWA) Support — Complete offline-first PWA implementation with service worker integration, IndexedDB/LocalStorage abstraction, optimistic UI updates, and offline-aware template directives. Includes comprehensive template tags (
{% djust_pwa_head %},{% djust_pwa_manifest %}), PWA mixins (PWAMixin,OfflineMixin,SyncMixin), and automatic synchronization when online. (#235) - Multi-Tenant SaaS Support — Production-ready multi-tenant architecture with flexible tenant resolution strategies (subdomain, path, header, session, custom, chained), automatic data isolation, tenant-aware state backends, and comprehensive template context injection. Includes
TenantMixinandTenantScopedMixinfor views. (#235) dj-pollattribute — Declarative polling for LiveView elements. Adddj-poll="handler_name"to any element to trigger the handler at regular intervals. Configurable viadj-poll-interval(default: 5000ms). Automatically pauses when the page is hidden and resumes on visibility change. (#269)DjustMiddlewareStack— New ASGI middleware for apps that don't usedjango.contrib.auth. Wraps WebSocket routes with session middleware only (no auth required). UpdatedC005system check to recognize bothAuthMiddlewareStackandDjustMiddlewareStack. (#265)- System check
C006— Warns whendaphneis inINSTALLED_APPSbutwhitenoisemiddleware is missing. (#259) startproject/startapp/newCLI commands —python -m djust new myappcreates a full project with optional features (--with-auth,--with-db,--with-presence,--with-streaming,--from-schema). Legacystartprojectandstartappcommands also available. (#266)djust mcp installCLI command — Automates MCP server setup for Claude Code, Cursor, and Windsurf. Triesclaude mcp addfirst (canonical for Claude Code), falls back to writing.mcp.jsondirectly. Merges with existing config, backs up malformed files, idempotent.- Simplified root element —
dj-viewis now the only required attribute on LiveView container elements. The client auto-stampsdj-rootanddj-liveview-rootat init time. Old three-attribute format still works. (#258) - Model
.pkin templates —{{ model.pk }}now works in Rust-rendered templates. Model serialization includes apkkey with the native primary key value. (#262) - Better Error Messages — Improved error messages for common LiveView event handler mistakes (missing
@event_handler, wrong method signature). (#248) LiveViewSmokeTestmixin — Automated smoke and fuzz testing for LiveView classes. (#251)- MCP server —
python manage.py djust_mcpstarts a Model Context Protocol server for AI assistant integration. Provides framework introspection, system checks, scaffolding, and validation tools. Used bydjust mcp installto configure Claude Code, Cursor, and Windsurf. djust_auditmanagement command — Security audit showing auth posture, exposed state, and handler signatures per view.djust_checkmanagement command — Django system checks for project validation. Gains--fixflag for safe auto-fixes and--format jsonfor enhanced output with fix hints.djust_schemamanagement command — Extract and generate Django models from JSON schema files.djust_ai_contextmanagement command — Generate AI-focused context files for LLM integrations.- AI documentation —
docs/ai/with focused guides for events, forms, JIT, lifecycle, security, and templates.docs/llms.txtanddocs/llms-full.txtfor LLM context. - Auto-build client.js from src/ modules — Pre-commit hook runs
build-client.shwhensrc/files change. (#211) - Keyed-mutation fuzz test generator — New proptest generator produces tree B by mutating tree A, exercising keyed diff paths more effectively. Proptest cases bumped from 500 to 1000. (#216, #217)
Changed
- BREAKING:
data-dj-*prefix stripping — Client-sideextractTypedParams()now strips thedj_prefix fromdata-dj-*attributes.data-dj-preset="dark"sends{preset: "dark"}instead of{dj_preset: "dark"}. Update handler parameter names accordingly:dj_foo→foo. - State Backends — Enhanced with tenant-aware isolation support (
TenantAwareRedisBackend,TenantAwareMemoryBackend).
Performance
- Batched
sync_to_asynccalls — Event handler processing now uses 2 thread hops instead of 4, saving ~1-4ms per event. (#277) - Eliminated JSON encode/decode roundtrip — Direct
normalize_django_value()Python-to-Python type normalization replaces 17json.loads(json.dumps(...))patterns. Saves 2-5ms per event for views with database objects. (#279) - Cached template variable extraction — Rust
extract_template_variables()results cached by content hash (SHA-256). Size-capped at 256 entries with automatic eviction. (#280) - Cached context processor resolution —
resolve_context_processors()results cached per settings configuration. Invalidated onsetting_changedsignal. (#281) - JIT short-circuit for non-DB views — Views without QuerySets or Models in context skip the entire JIT serialization pipeline. Saves ~0.5ms per event for simple views. (#278)
- Slimmer debug payload — Event responses send only state variables; handler metadata moved to initial mount as static data. ~68% smaller debug payloads (~25KB → ~8KB per event).
Fixed
- Inline args on form events —
dj-change,dj-input,dj-blur,dj-focusnow parse inline arguments (e.g.,dj-change="toggle(3)") before sending to server. Also fixed state change detection to use deep copy comparison, catching in-place mutations. - Error overlay on intentional disconnect — Suppress "WebSocket Connection Failed" overlay during TurboNav navigation via
_intentionalDisconnectflag. - VDOM patch failure recovery — When VDOM patches fail, the client requests recovery HTML on demand instead of reloading the page. Uses DOM morphing to preserve event listeners and form state. (#259)
- HTTP Fallback Protocol —
post()now accepts the HTTP fallback format where the event name is in theX-Djust-Eventheader and params are flat in the body JSON. (#255) - Debug panel HTTP-only mode — POST responses include
_debugpayload whenDEBUG=True, enabling the debug panel in HTTP-only mode. (#267) - Silent LiveView config failures — Client JS now shows helpful
console.errorwhen no LiveView containers are found. Added system checkV005for modules not inLIVEVIEW_ALLOWED_MODULES. (#257) - HTTP-only mode session state on GET —
get()now saves view state to the session immediately whenuse_websocket: False. (#264) use_websocket: Falseclient-side enforcement — Setting now actually prevents WebSocket connections. (#260)- DOM morphing preserves event listeners —
html_updatenow uses morphdom-style DOM diffing instead ofinnerHTML. (#236) - Textarea newlines preserved — Template whitespace stripping no longer collapses newlines inside
<textarea>elements. (#236) - PresenceMixin crash without auth —
track_presence()now checks forrequest.userbefore accessing it. (#236) _skip_rendersupport in server_push —server_push()now checks_skip_render, preventing phantom renders and VDOM version mismatches. (#236)- Client-side SetText mis-targets after keyed MoveChild — MoveChild patches now include
child_dfordata-dj-idresolution. (#225) - VDOM diff/patch round-trip on keyed child reorder — Patches now processed level-by-level (shallowest parent first). (#212)
- apply_patches djust_id-based resolution — Resolves parent nodes by
djust_idinstead of path-based traversal. (#216) - Diff engine keyed+unkeyed interleaving — Emits
MoveChildpatches for unkeyed element children in keyed contexts. (#219) - Text node targeting after keyed moves —
SetTextpatches carrydjust_idwhen available;sync_idspropagates IDs to text nodes. (#221) - Tag registry test pollution —
clear_tag_handlers()now restores built-in handlers in teardown. (#261)
Security
- HTTP POST handler dispatch gating —
post()now enforces the same security model as the WebSocket path: only@event_handler-decorated methods can be invoked. Validates event names withis_safe_event_name()to block dunders and private methods. - Auto-escaping in Rust template engine —
SafeStringvalues propagated to Rust for proper auto-escaping. - HTML-escaped
urlizeandunordered_listfilters — Both filters now escape their output to prevent XSS. (#254) - Template tag XSS prevention — All PWA template tags now use
format_html()andescape()instead ofmark_safe()with f-string interpolation. - Sync endpoint hardening — Removed
@csrf_exemptfromsync_endpoint_view. Added authentication requirement, payload validation, and safe field extraction. - Silent exception elimination — All
except: passpatterns replaced with appropriate logging calls. - Production JS hardened — All
console.logcalls guarded behinddjustDebugflag.
Removed
_allowed_eventsclass attribute — The backwards-compatibility escape hatch that allowed undecorated methods to be called via WebSocket or HTTP POST has been removed. All event handlers must now use the@event_handlerdecorator.
0.2.2 - 2026-02-01
Fixed
- Stale Closure Args on VDOM-Patched Elements — After deleting a todo, the remaining button's click handler sent the wrong
_args(stale closure from bind time) becauseSetAttributepatches updated thedj-clickDOM attribute but not the listener closure. Event listeners now re-parsedj-*attributes from the DOM at event time. Also setsdj-*as DOM attributes increateNodeFromVNodeand marks elements as bound to prevent duplicate listeners. (#205) - VDOM: Non-breaking Space Text Nodes Stripped — Rust parser stripped
-only text nodes (used in syntax highlighting) becausechar::is_whitespace()includes U+00A0. Now preserves\u00A0text nodes in parser,to_html(), and client-side path traversal. Also addssync_ids()to prevent ID drift between server VDOM and client DOM after diffing, and 4-phase patch ordering matching Rust'sapply_patches(). (#199) - CSRF Token Lookup on Formless Pages — Pages without a
<form>element failed to send CSRF tokens with WebSocket events. Token lookup now falls back to thecsrftokencookie. (#210) - Codegen Crash on Numeric Index Paths — Template expressions like
{{ posts.0.url }}produced paths starting with a numeric index (0.url), generating invalid Python (obj.0). Codegen now skips numeric-leading paths since list items are serialized individually. - JIT Serialization Pipeline — Fixed multiple issues in JIT auto-serialization: (#140)
- M2M
.all()traversal now generates correct iteration code in codegen serializers @propertyattributes are now serialized via Rust→Python codegen fallback when Rust can't access themlist[Model]context values (not just QuerySets) now receive full JIT optimization withselect_related/prefetch_related- Nested dicts containing Model/QuerySet values are now deep-serialized recursively
_djust_annotationsmodel class attribute for declaring computed annotations (e.g.,Count) applied during query optimization{% include %}templates are now inlined for variable extraction, so included template variables get JIT optimization- Rust template parser now correctly prefixes loop variable paths (e.g.,
item.fieldinside{% for item in items %})
- M2M
{% include %}After Cache Restore —template_dirswas not included in msgpack serialization ofRustLiveView. After a cache hit, the restored view had empty search paths, causing{% include %}tags to fail with "Template not found". Now callsset_template_dirs()on both WebSocket and HTTP cache-hit paths.- VDOM Replace Sibling Grouping — Fixed
data-djust-replaceinserting children into wrong parent when the replace container has siblings.groupPatchesByParent()now uses the full path for child-operation patches, andgroupConsecutiveInserts()checks parent identity before batching. (#144) - VDOM Replace Child Removal — Fixed
data-djust-replacenot removing old children before inserting new ones, causing duplicate content on re-render. (#142, #143) - Context Processor Precedence — View context now takes precedence over context processors. Previously, context processors could overwrite view-defined variables (e.g., Django's messages processor overwriting a view's
messagesvariable). - VDOM Keyed Diff Insert Ordering — Fixed
apply_patchesfor keyed diff insert ordering where items were inserted in the wrong position. (#154) - VDOM MoveChild Resolution — Fixed
MoveChildinapply_patchby resolving children viadjust_idinstead of index. (#150) - Debug Toolbar: Received WebSocket Messages Not Captured — Network tab now captures both sent and received WebSocket messages by intercepting the
onmessageproperty setter (not justaddEventListener). (#188) - Debug Toolbar: Events Tab Always Empty — Events tab now populates by extracting event data from sent WebSocket messages and matching responses, replacing the broken
window.liveViewhook. (#188) - Debug Panel: Handler Discovery, Auto-loading, Tab Crashes — Handler discovery now finds all public methods;
debug-panel.jsauto-loads; handler dict normalized to array; retroactive WebSocket hooking for late-loading panels. (#191, #197)
Added
- Debug Panel: Live Debug Payload — When
DEBUG=True, WebSocket event responses now include a_debugfield with updated variables, handlers, patches, and performance metrics. (#191) - Debug Toolbar: Event Filtering — Events tab filter controls to search by event/handler name and filter by status. (#180)
- Debug Toolbar: Event Replay — Replay button (⟳) that re-sends events through the WebSocket with original params. (#181)
- Debug Toolbar: Scoped State Persistence — Panel UI state scoped per view class via localStorage. (#182)
- Debug Toolbar: Network Message Inspection — Directional color coding and copy-to-clipboard for expanded payloads. (#183)
- Debug Toolbar: Test Harness — Integration tests against the actual
DjustDebugPanelclass. (#185) - VDOM Proptest/Fuzzing — Property-based testing for the VDOM diff algorithm with
proptest. (#153) - Duplicate Key Detection — VDOM keyed diff now warns on duplicate keys. (#149)
- Branding Assets — Official logo variants (dark, light, icon, wordmark, transparent). (#208, #213)
Deprecated
@eventdecorator alias — The@eventshorthand is deprecated in favor of@event_handler.@eventwill be removed in v0.3.0. A deprecation warning is emitted at import time. (#141)
Changed
- Internal: LiveView Mixin Extraction — Refactored monolithic
live_view.pyinto focused mixins:RequestMixin,ContextMixin,JITMixin,TemplateMixin,RustBridgeMixin,ComponentMixin,LifecycleMixin. No public API changes. (#130) - Internal: Module Splits — Split
client.jsinto source modules with concat build, extractedwebsocket_utils.py,session_utils.py,serialization.py, splitstate_backend.pyintostate_backendspackage, splittemplate_backend.pyintotemplatepackage. (#124, #125, #126, #128, #129) - Dependencies — Upgraded uuid 1.19→1.20, thiserror 1→2, bincode 1→2, happy-dom 20.3.7→20.4.0, actions/setup-python 5→6, actions/upload-artifact 4→6, actions/checkout 4→6, softprops/action-gh-release 1→2
0.2.1 - 2026-01-29
Security
- WebSocket Event Security Hardening - Three-layer defense for WebSocket event dispatch: (#104)
- Event name guard — regex pattern filter (
^[a-z][a-z0-9_]*$) blocks private methods, dunders, and malformed names beforegetattr() @event_handlerdecorator allowlist — only methods decorated with@event_handler(or listed in_allowed_events) are callable via WebSocket. Configurable viaevent_securitysetting ("strict"default,"warn","open")- Server-side rate limiting — per-connection token bucket algorithm with configurable rate/burst. Per-handler
@rate_limitdecorator for expensive operations. Automatic disconnect after repeated violations (close code 4429) - Per-IP connection limit — process-level
IPConnectionTrackerenforces a maximum number of concurrent WebSocket connections per IP (default: 10) and a reconnection cooldown after rate-limit disconnects (default: 5 seconds). Configurable viamax_connections_per_ipandreconnect_cooldowninrate_limitsettings. SupportsX-Forwarded-Forheader for proxied deployments. (#108, #121) - Message size limit — 64KB default (
max_message_sizesetting)
- Event name guard — regex pattern filter (
Documentation
- Added migration guide for
@event_handlerdecorator requirement and strict mode upgrade path (#105, #122) - Added
@event_handlerdecorator to all example demo view handler methods
Added
is_event_handler(func)— check if a function is decorated with@event_handler@rate_limit(rate, burst)— per-handler server-side rate limiting decorator_allowed_eventsclass attribute — escape hatch for bulk allowlisting without decorating each methodLIVEVIEW_CONFIGsettings:event_security,rate_limit(includingmax_connections_per_ip,reconnect_cooldown),max_message_size
0.2.0 - 2026-01-28
Added
- Template
and/or/inOperators -{% if %}conditions now supportand,or, andinboolean/membership operators with correct precedence and chaining. (#103)
Fixed
-
Pre-rendered DOM Whitespace Preservation - WebSocket mount no longer replaces
innerHTMLwhen content was pre-rendered via HTTP GET. Instead,data-dj-idattributes are stamped onto existing DOM elements, preserving whitespace in code blocks and syntax-highlighted content. (#99) -
VDOM Keyed Diffing - Unkeyed children in keyed diffing contexts are now matched by relative position among unkeyed siblings, eliminating spurious insert+remove patch pairs when keyed children reorder. (#95, #97)
-
Event Handler Attributes Preserved -
dj-*event handler attributes are no longer removed during VDOM patching. (#100) -
Model List Serialization - Lists of Django Model instances are now properly serialized on GET requests. (#103)
-
Mount URL Path - WebSocket mount requests now use the actual page URL instead of a hardcoded path. (#95)
Changed
- Dependencies - Upgraded html5ever 0.27→0.36, markup5ever_rcdom 0.3→0.36, vitest 2.x→4.x, actions/download-artifact 4→7. (#101, #102, #43)
Developer Experience
- VDOM Debug Tracing -
debug_vdomDjango config is now bridged to Rust VDOM tracing. Mixed keyed/unkeyed children emit developer warnings. (#97)
0.2.0a2 - 2026-01-27
Changed
- Internal: DRY Refactoring - Reduced ~275 lines of duplicate code across the codebase through helper function extraction. These are internal improvements that don't affect the public API. (#93, #94)
getComponentId()- DOM traversal for component ID lookup (client.js)buildFormEventParams()- Form event parameter building (client.js)send_error()- WebSocket error response helper (websocket.py)_send_update()- WebSocket patch/HTML response helper (websocket.py)_create_rust_instance()- Rust component instantiation (base.py)_render_template_with_fallback()- Template rendering with Rust→Django fallback (base.py)_make_metadata_decorator()- Decorator factory for metadata-only decorators (decorators.py)
0.2.0a1 - 2026-01-26
Changed
-
BREAKING: Event Binding Syntax - Standardized all event bindings to use
dj-prefix instead of@prefix. This affects all event attributes:@click→dj-click,@input→dj-input,@change→dj-change,@submit→dj-submit,@blur→dj-blur,@focus→dj-focus,@keydown→dj-keydown,@keyup→dj-keyup,@loading.*→dj-loading.*. Benefits: namespaced attributes, no conflicts with Vue/Alpine, no CSS selector escaping required. (#68) -
BREAKING: Component Consolidation - Removed legacy
python/djust/component.py. Usedjust.Componentwhich now imports fromcomponents/base.py. (#89) -
BREAKING: Method Rename -
LiveComponent.get_context()→get_context_data()for Django consistency. (#89) -
BREAKING: Decorator Attributes Removed - Deprecated decorator attributes removed:
_is_event_handler,_event_name,_debounce_seconds,_debounce_ms,_throttle_seconds,_throttle_ms. Use_djust_decoratorsdict instead. (#89) -
BREAKING: Data Attributes Renamed - Standardized data attribute naming for consistency:
dj-liveview-root→dj-rootdata-live-view→dj-viewdata-live-lazy→dj-lazydata-dj→data-dj-id(#89)
-
BREAKING: WebSocket Message Types - Renamed message types for consistency:
connected→connectmounted→mounthotreload.message→hotreload(#89)
Added
-
LiveComponent Methods - Added missing methods to
LiveComponent:_set_parent_callback(),send_parent(),unmount(). (#89) -
Inline Template Support -
LiveComponentnow supports inlinetemplateattribute for template strings, in addition totemplate_namefor file-based templates. (#89) -
Form Components Export -
ForeignKeySelectandManyToManySelectare now exported fromdjust.components. (#89)
Fixed
-
{% elif %}Tag Support: Template parser now correctly handles{% elif %}conditionals. Previously, elif branches fell through to the unknown tag handler and rendered all branches instead of just the matching one. (#80) -
Template Include Fallback - Component
render()methods now fall back to Django templates when Rust template engine fails (e.g., for{% include %}tags). (#89)
0.1.8 - 2026-01-25
Fixed
- Nested Block Inheritance: Fixed template inheritance for nested blocks. When a child template overrides a block that is nested inside another block in the parent (e.g.,
contentinsidebody), the override is now correctly applied. (#71)
0.1.7 - 2026-01-25
Added
- Tag Handler Registry: Extensible system for custom Django template tags in Rust. Register Python callbacks for tags like
{% url %}and{% static %}with ~100-500ns overhead per call. Built-in tags (if, for, block) remain zero-overhead native Rust. Includes ADR documenting architecture decisions. (#65) - Comparison Operators: Template conditions now support
>,<,>=,<=operators in addition to==and!=. (#65) - Enhanced
{% include %}Tag: Full support forwithclause (pass variables) andonlykeyword (isolate context). (#65) - Performance Testing Infrastructure: Comprehensive benchmarking with Criterion (Rust) and pytest-benchmark (Python). New Makefile commands:
make benchmark,make benchmark-quick,make benchmark-e2e. Enables tracking performance across releases and detecting regressions. (#69) - Inline Handler Arguments: Event handlers now support function-call syntax with arguments directly in the template attribute. Use
dj-click="handler('arg')"instead ofdj-click="handler" data-value="arg". Supports strings, numbers, booleans, null, and multiple arguments. (#67)
Fixed
- Async Event Handlers: WebSocket consumer now properly supports
async defevent handlers. Previously only synchronous handlers worked correctly. (#63)
Performance
- Dashboard render: ~37µs (27,000 renders/sec)
- Tag handler overhead: ~100-500ns per call
- Template variable substitution: ~970ns
- 50-row data table: ~188µs
0.1.6 - 2026-01-24
Added
{% url %}Tag Support: Django's{% url %}template tag is now fully supported with automatic Python-side URL resolution. Supports named URLs, namespaced URLs, and positional/keyword arguments. (#55){% include %}Tag Support: Fixed template include functionality by passing template directories to the Rust engine. Included templates are now correctly resolved from configured template paths. (#55)urlencodeFilter: Added theurlencodefilter for URL-safe encoding of strings. Supports encoding all characters or preserving safe characters. (#55)- Comparison Operators in
{% if %}Tags: Added support for>,<,>=,<=comparison operators in conditional expressions. (#55) - Auto-serialization for Django Types: Context variables with Django types (datetime, date, time, Decimal, UUID, FieldFile) are now automatically serialized for Rust rendering. No manual JSON conversion required. (#55)
- Lazy Hydration: LiveView elements can now defer WebSocket connections until they enter the viewport or receive user interaction. Use
dj-lazyattribute with modes:viewport(default),click,hover, oridle. Reduces memory usage by 20-40% per page for below-fold content. (#54) - TurboNav Integration: LiveView now works seamlessly with Turbo-style client-side navigation. WebSocket connections are properly disconnected on navigation and reinitialized when returning to a page. (#54)
Changed
- AST Optimization: Template parser now merges adjacent Text nodes during AST optimization, reducing allocations and improving render time by 5-15%. Comment nodes are also removed during optimization as they produce no output. (#54)
Fixed
- Nested Block Inheritance: Fixed template inheritance for nested blocks (e.g.,
docs_contentinsidecontent). Block overrides are now recursively applied to merged content, ensuring deeply nested blocks are correctly resolved. (#57) - Form Validation First-Click Issue: Added
parse_html_continue()function to maintain ID counter continuity across parsing operations. Prevents ID collisions when inserting dynamically generated elements (like validation error messages) that caused first-click validation issues. (#54) - Whitespace Preservation: Whitespace is now preserved inside
<pre>,<code>,<textarea>,<script>, and<style>elements during both Rust parsing and client-side DOM patching. (#54)
Security
- pyo3 Upgrade: Upgraded pyo3 from 0.22 to 0.24 to address RUSTSEC-2025-0020 (buffer overflow vulnerability in
PyString::from_object). (#55)
0.1.5 - 2026-01-23
Added
- Context Processor Support: LiveView now automatically applies Django context processors configured in
DjustTemplateBackend. Variables likeGOOGLE_ANALYTICS_ID,user,messages, etc. are now available in LiveView templates without manual passing. (#26)
Fixed
- VDOM Cache Key Path Awareness: Cache keys now include URL path and query string hash, preventing render corruption when navigating between views with different template structures (e.g.,
/emails/vs/emails/?sender=1). (#24)
0.1.4 - 2026-01-22
Added
- Initial public release
- LiveView reactive server-side rendering
- Rust-powered VDOM engine (10-100x faster than Django templates)
- WebSocket support for real-time updates
- 40+ UI components (Bootstrap 5 and Tailwind CSS)
- State management decorators (
@state,@computed,@debounce,@optimistic) - Form handling with real-time validation
- Testing utilities (
LiveViewTestClient, snapshot testing)
0.1.3 - 2026-01-22
Fixed
- Bug fixes and stability improvements