This is a pre-release. djust 1.0.0 has shipped since: read the djust 1.0.0 release notes.
Before you upgrade, read the upgrade guide.
Added
LiveViewTestClient.assert_http_ws_djid_parity()— test-infra harness for HTTP-GET vs WebSocket-mountdj-idparity (#1642). Builds two independent instances of a view (one exercising the HTTP initial-page path viarender_full_templatethenrender_with_diff; one the WebSocket-mount baseline viarender_with_diffonly) and asserts they assign an identicaldj-idbaseline in thedj-rootsubtree. A divergence is thegetNodeByPath → nullfirst-event patch-miss shape investigated in #1641; this locks the #1370 "marker IDs match between the initial HTTP DOM and subsequent WS diffs" invariant against regression and lets the divergence hypothesis be tested framework-side. Seedocs/website/guides/testing.md.
Fixed
djust new --with-dbprojects now create their tables on deploy (#1637). The scaffold ranmigrate --run-syncdbbut nevermakemigrations, so a--with-dbapp shipped with no migrations.--run-syncdbcreated the tables on the developer's machine (masking the gap), but a deploy runsmigratewithout--run-syncdb, so the app's tables were never created and the first request 500'd (no such table: <app>_<model>).djust newnow runsmakemigrationsbeforemigrate(generating the app'smigrations/__init__.py+0001_initial.py, which ship in the deploy artifact), and explicitly writes<app>/migrations/__init__.pyfor model-bearing apps so the migrations directory is a real package — not a namespace package the migration loader silently skips — even when--no-installis used.client.jsno longer throws a re-declarationSyntaxErroronlive_redirect/ bfcache restore (#1635).live_redirectkeeps the document alive and morphdom can re-attach the<script src="client.js">tag; a bfcachepageshowre-init follows the same path. Two classic<script>executions share one global lexical environment, so top-levelconst/let/classin the bundle re-declared at parse time on the second execution —Identifier '_TEXT_INPUT_TYPES' has already been declared— before the runtimewindow._djustClientLoadedguard could run. The bundle is now wrapped in a single IIFE (scripts/build-client.sh), so every top-level declaration is function-scoped; a second execution builds a fresh scope and the runtime guard short-circuits the re-init. Public API is still exported via explicitwindow.*/globalThis.djust.*assignments inside the IIFE — no observable change.VDOM index patches no longer mis-resolve when a template has a plain HTML comment among siblings (#1640). The client's
getSignificantChildrencounted ALL comment nodes, whilegetNodeByPath(path traversal) and the Rust VDOM parser count onlydj-if-family boundary markers and drop every other comment. Index-based patches (InsertChild/RemoveChild/MoveChild) resolve their index viagetSignificantChildren, so a regular<!-- comment -->among siblings shifted the client index one slot off the server's — inserting/removing/moving the wrong node.getSignificantChildrennow uses the samedj-if-only predicate asgetNodeByPath.dj-ifmarkers are still counted (server parity); regular comments are dropped.Per-event object-permission check no longer false-denies on the first event (#1638). The per-event re-check (
_validate_event_security) calledcheck_object_permissionsynchronously from anasync def, while the mount path wraps it insync_to_async. The developer's syncget_object()— the canonical ADR-017 pattern doingModel.objects.get(...)— then raisedSynchronousOnlyOperationin the event loop, and the fail-closedexcepttranslated it into a spurious "Access denied"permission_deniedframe on the first event of every URL-bound LiveView with a syncget_object(). The per-event call is now wrapped insync_to_async, mirroring the mount path. Who's affected: any LiveView following the documented ADR-017 object-permission pattern (get_object()doing a sync ORM read) on djust ≥ v0.9.5-1b. No migration needed — the fix is transparent; the false denial simply stops.live_redirect()/live_patch()from a state-unchanging handler now reach the client (#1643). When an event handler reassigns no public attr (e.g. it only writes to a DB model, then callslive_redirect()),handle_eventtakes its auto-skip-render branch and returns via_send_noop. That branch — and the batched_dispatch_single_eventskip branch, plus theserver_pushanddb_notifybroadcast paths — flushed onlypush_events/flash/page_metadata/pending_layout/deferred, silently dropping queuednavigation/accessibility/i18ncommands. The result: the handler ran (DB write succeeded) but the browser URL never changed. Root cause was duplication — the turn-end flush sequence was hand-copied across ~10 sites with inconsistent subsets. Consolidated every turn-end path onto a single_flush_all_pending()helper that flushes all queued side-effects in canonical order (draining is idempotent, so paths that also go through_send_updateare unaffected). Net reduction of ~29 lines inwebsocket.py; also closes the same latent gap on theserver_pushanddb_notifybroadcast paths.start_asynccompletion now refreshes the VDOM recovery baseline (#1636). When a client VDOM patch fails (e.g. an{% if %}block that adds a sibling), the client requestshtml_recoveryand the server serves — then clears —_recovery_html(one-time use).LiveViewConsumer._run_async_workre-renders and sends patches when astart_asyncbackground callback completes, but — unlikehandle_eventandserver_push(the #1202 fix) — it never updated_recovery_html/_recovery_version. After a recovery had consumed the baseline, an async-triggered patch that also failed on the client found_recovery_html=None, got back "Recovery HTML unavailable", and the view froze at the transitional state (e.g.Status: fetching) even though the backend pipeline had advanced._run_async_worknow sets the recovery baseline afterrender_with_diff()in both the patches branch and the full-HTML fallback branch, mirroring the other two render paths, so async-callback state pushes keep reaching the client across patch-failure/recovery cycles.