This is a pre-release. djust 0.9.2 has shipped since: read the djust 0.9.2 release notes.
Fixed
dj-transitionnow accepts 1-token short form, matching documented grammar. Closes #1273. Thedj-transition-groupdocs at43-dj-transition-group.js:22-23advertise short form likedj-transition-group="fade-in | fade-out"where each half can be a 1-token spec — but_parseSpecat41-dj-transition.js:45-60required 3 tokens, so the ENTER side was silently rejected and no animation fired. Fix: extend_parseSpecto accept 1-token form, return{single: <class>};_runTransitionhandles single by applying the class on next frame and waiting fortransitionend. 2-token form remains rejected as ambiguous (matchesdj-remove). 2 new regression cases inTestParseSpecAcceptsShortForm+ behavioral test (tests/js/dj_transition.test.js).AsyncResultnow serializes to a dict templates can navigate. Closes #1274.assign_async()returnsAsyncResultinstances that templates expect to read as{{ users.loading }},{{ users.ok }},{{ users.failed }},{{ users.result }},{{ users.error }}. Before this fix, neithernormalize_django_valuenorDjangoJSONEncoder.defaulthad anAsyncResultbranch — the value fell through tostr(), producing"AsyncResult(loading=True, ...)"which templates couldn't navigate. Result: everyassign_asyncdemo rendered blank. Fix: newAsyncResult.to_dict()method + register in both serializer paths;normalize_django_valuerecurses into the dict so a non-primitiveresultpayload (Django Model, datetime, Decimal nested in result) is normalized too. 11 regression cases inpython/djust/tests/test_async_result_serializer.py.Form submit now flushes pending debounced
dj-inputhandlers before dispatching. Closes #1278. Text/email/password inputs withdj-inputdefaulted to 300ms debounce; a user who typed and immediately clicked submit raced the submit handler past the pending input events. Views that depended on server-side state populated bydj-inputhandlers (e.g.,WizardMixin'swizard_step_data) saw stale state at submit time. Fix:debounce()now exposes a.flush()method; new_flushPendingDebouncesInForm(form)iterates the form's[dj-input]descendants and flushes any pending wrappers;_handleDjSubmitcalls it before reading FormData / dispatching. 4 regression cases intests/js/dj_submit_debounce_flush.test.js.dj-dialogclient-close (ESC/backdrop/dialog.close()) now syncs back to server. Closes #1267. Previouslydj-dialogwas one-way (server→client); the user closing a dialog client-side left server state believing it was still open. Re-opening from the server became a no-op because re-assertingdj-dialog="open"wasn't a value change. Fix: newdj-dialog-close-event="..."attribute opts into a nativecloseevent listener that dispatches the configured event name to the server. Idempotent across re-syncs (WeakMap guard); reads attribute at fire time so morph updates take effect. 5 new regression cases intests/js/dj_dialog.test.js.SSE transport: EventSource and dispatch POST now send Django session cookie. Closes #1277. Authenticated views over SSE failed
check_view_authon every mount because the EventSource GET (and the message POST) didn't carry credentials. Result: infinite mount→navigate loop on authenticated views. Fix:03b-sse.js:69opens EventSource with{withCredentials: true};03b-sse.js:367sendMessagePOST setscredentials: 'include'. 2 new regression cases intests/js/sse-transport.test.js.mount()lifecycle: queued async work and push events are now drained after the mount frame. Closes #1280 (assign_async()/start_async()called frommount()never resolved over WebSocket — view stayed at initial loading-state HTML forever) and #1283 (push_event()called frommount()oron_mounthooks queued events that never reached the client). Both root at the same site:LiveViewConsumer.handle_mount()ended withsend_json(response)without draining_async_tasksor_pending_push_events. The fix mirrors the established pattern inhandle_event()/_flush_deferred_activity_events(): send the response frame, then drain push events, then dispatch async work. 3 regression cases inTestHandleMountSourceShape(python/djust/tests/test_handle_mount_drains_queues.py).data_tableintegration restored over WebSocket — emit defaults renamed to matchon_table_*mixin handler convention. Closes #1275 (tag emitted 23 event names that didn't match any handler), #1291 (pagination handlers entirely missing from the mixin), #1279 (handlers mutate state but never refresh rows). Single root cause: the WS dispatcher does exact-matchgetattr(view, event_name, None)(websocket_utils.py:173), but the tag-emit defaults previously used baretable_*strings while DataTableMixin useson_*Phoenix-style handler names — so every default WS interaction returned "no handler found". Fix: rename tag-emit defaults across 4 files (92 lines:templatetags/djust_components.py,mixins/data_table.pyclass-level attrs +_PRE_MOUNT_TABLE_CONTEXT,components/rust_handlers.py,templatetags/_forms.py); addon_table_prev/on_table_nexthandlers (clamped to[1, table_total_pages]); callrefresh_table()from sort/search/filter/page/prev/next handlers (selection handler deliberately exempt — UI state). 15 regression cases inTestDataTableEmitToHandlerCrossReference,TestPaginationHandlersExist,TestRowAffectingHandlersCallRefresh(python/djust/tests/test_data_table_handler_contracts.py). Subclasses that overrodetable_X_eventclass attrs are unaffected — only the bare-default path was broken.@actionno longer re-raises after recording exception state. Closes #1276. The decorator's docstring promised templates could read{{ <name>.error }}after an exception, but the implementation re-raised — the dispatcher's exception-frame path then bypassed the re-render and the template never saw the recordederrorfield. Fix: catchException(notBaseException), record state, log at ERROR level vialogger.exception, return None.BaseExceptionsubclasses (KeyboardInterrupt,SystemExit,GeneratorExit) still propagate by Python convention. 8 new regression cases inTestActionExceptionDoesNotPropagate+TestActionSuccessRecordsStateTestActionLazyInitializesActionState(python/djust/tests/test_action_decorator_contract.py); 6 existing tests intest_action_decorator.pyupdated to the new contract. Docstring atdecorators.py:262-272rewritten to match. Behavior change: code that wraps@actioncalls intry/exceptto handle the re-raise now sees a clean return. Mirror the old behavior by re-raising explicitly inside the handler.
Documentation
Lifecycle Coverage Audit + Decorator/Tag Contract Audit (
docs/audits/lifecycle-2026-05.md,docs/audits/decorator-contract-2026-05.md). Two companion audit docs modeled on the v0.9.2-4 VDOM audit. Document the canonical state-type × lifecycle-hook matrix and the decorator/tag-name dispatch contract, surfaced from 10 downstream consumer bug reports (#1267, #1273-#1281). The lifecycle audit catalogues 8 ranked weaknesses including the central control-flow gaps inmount()(#1280, #1281). The decorator/tag audit catalogues 8 weaknesses including thedata_tabletag emitting 23 event names that don't match any DataTableMixin handler (#1275 generalized). Each audit ships with a 4-phase improvement roadmap, test gaps, strategic observations, and a companion canon update forCLAUDE.md/PR-checklist. Pre-staged issues filed for each not-yet-tracked weakness (#1283-#1291). Audit-driven Phase 1 fixes blocking v0.9.2 stable will land in the v0.9.2-5 drain bucket; Phase 2/3 fixes targeted for v0.9.3.Production Deployment guide extended with Tier 1/2/3 patterns (
docs/website/guides/deployment.md). Adds 8 new sections to the canonical deployment guide based on patterns surfaced from real-world djust deployments:- Channel Layer (cross-process push) — separate concern from
DJUST_STATE_BACKEND, required when any view usespush_to_view, presence, or cursor tracking. - Database Connection Pooling — three-layer guidance (
CONN_MAX_AGE, PgBouncer, RDS Proxy), with the LISTEN/NOTIFY caveat for transaction-mode pooling. - Celery Integration — broker choice (Redis vs SQS), pool choice (prefork vs gevent), beat-singleton invariant, gevent monkey-patch gotcha, queue-depth-based worker auto-scaling.
- Static and Media Files — cloud-agnostic CDN options, S3 + CloudFront config,
ASGI_SERVE_STATIC=Falseopt-out for offloading static-file serving from the ASGI server. - WebSocket stickiness on AWS ALB — the simpler "stick on Django
sessionid" pattern as an alternative to a custom application-set cookie. - Sizing and Scaling Tiers — concrete vCPU/RAM recommendations indexed to concurrent active users (≤50, 50-500, >500), with explicit "when to escalate" triggers.
- "What's Already Production-Ready in djust" — anti-recommendation list (Redis state,
channels_redis,sync_to_async,transaction.on_commit, Origin check, HSTS) so users don't re-evaluate canonical patterns on every deployment. - Extended Gunicorn+Uvicorn workers section with concrete production CMD + flag rationale (
-wsizing,--timeout 120,--keep-alive 5). Cloud-agnostic where possible; AWS as canonical example with PgBouncer / GCS / Cloudflare R2 noted in parallel.
- Channel Layer (cross-process push) — separate concern from
Developer Experience
- Pipeline-template canon: Stage 7 self-applicability check for canon PRs (#1248). New optional checklist item in
.pipeline-templates/{feature,bugfix}-state.jsonStage 7 fires when a PR adds new mandatory rules. Asks: (a) does the new rule false-positive on this PR's own diff? (b) would the new rule have caught the originating bug at the stage it adds? Both must be explicitly answered. v0.9.2-2 retro Action Tracker #206. - Pipeline-template canon: Stage 5/9/10 bundling check (#1251). New mandatory checklist item runs
git diff --cached --statimmediately beforegit commitand verifies the staged line counts match the planned scope. Catches the failure mode wheregit add <file>silently bundles pre-existing uncommitted modifications (the pattern that hit pipeline-skill commitbf1a67f, silently bundling 130 unintended lines). v0.9.2-2 retro Action Tracker #209. - Audit script: extract retro-marker regex to shared constants module (#1249). Created
scripts/lib/retro_markers.pywithRETRO_MARKER_REGEX. The audit script (scripts/audit-pipeline-bypass.py) now imports the canonical constant rather than embedding the literal. Stage 14subagent_prompttext in both pipeline templates references the script-canonical file rather than re-defining the regex. Single source of truth across consumers. v0.9.2-2 retro Action Tracker #207. 4 unit tests atscripts/lib/test_retro_markers.py. - Audit script: scan direct-to-main commits +
Audit-bypass-reason:trailer support (#1250). The retro-gate audit GHA previously scanned merged PRs only; direct commits to main bypassed it (e.g., the v0.9.2-2 milestone-open commit18e5b117). The audit now also lists direct-to-main commits since the lookback window, filters out PR-squash commits via(#NNN)subject suffix, and honors anAudit-bypass-reason: <text>commit-message trailer for legitimate exemptions (e.g., docs-only ROADMAP updates per the pipeline-drain skill). v0.9.2-2 retro Action Tracker #208.
Fixed
- VDOM: mixed keyed/unkeyed children diff round-trip correctness (#1260). Surfaced by proptest during v0.9.2rc1 pre-flight. The LIS optimization in
diff_keyed_children(crates/djust_vdom/src/diff.rs) skipped emittingMoveChildpatches for keyed children whose trivial-length-1 LIS made them appear "in place" — relying on other patches' implicit position shifts to land them at the correct absolute index. This works for fully-keyed sibling lists (where all moves coordinate via absolute indices) but breaks when unkeyed siblings are interleaved (their patches use positions relative to other unkeyed nodes only). The keyed child ended up stranded at an arbitrary index after all patches applied. Fix detectshas_unkeyed_siblingsupfront; in the mixed case, falls back to "always emit MoveChild whenold_idx != new_idx" instead of the LIS-implicit-position optimization. The fully-keyed path is unchanged. Audit weakness #5/#6 (rated 🟡 with warnings only) upgraded to 🟠 by this fuzz finding; this is the actual fix. 4 deterministic regression tests incrates/djust_vdom/tests/test_mixed_keyed_unkeyed_reorder_1260.rs- permanent proptest seed in
fuzz_test.proptest-regressions.
- permanent proptest seed in