Notechondria

Version: 0.1.70 Build Date: 2026-04-23T00:00

What's Changed

Bug — frontend API URL reverted on every theme / state change

  • Root cause: every app's app_shell.dart had client: widget.client ?? HttpNotechondriaClient() inline in the _NotechondriaAppState.build() method. setState on that state (e.g. a theme change from _handleThemeChanged) calls build() again and re-evaluates the ??, minting a fresh HttpNotechondriaClient whose _baseUrl is the compile-time default. That new client replaces the one whose _loadLocalState had called updateBaseUrl(saved), so subsequent HTTP calls went to the default host (notechondria.trance-0.com) regardless of the note.zheyuanwu.com the user had saved in App Preferences.
  • Fix: cache the client in a late final NotechondriaClient _client = widget.client ?? HttpNotechondriaClient() field. Construction happens once per app-instance lifetime; rebuilds read the stored reference. Applied in all three apps: editor_app, planner_app, portal_app. Header comment on each spells out the bug so the next rewrite doesn't reintroduce it.

Bug — backend 400 DisallowedHost for the user's custom domain

  • Root cause: settings.py computed _default_hosts with BACKEND_CUSTOM_DOMAIN + RENDER_EXTERNAL_HOSTNAME folded in, then did ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", _default_hosts). If DJANGO_ALLOWED_HOSTS was explicitly set in env (as it is in Northflank's Secret Group), the explicit value silently superseded _default_hosts — so setting BACKEND_CUSTOM_DOMAIN=notechondria.trance-0.com had no effect on ALLOWED_HOSTS. Any request arriving with that Host header (because the user's DNS points the custom domain at the backend) was rejected with django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'notechondria.trance-0.com'. You may need to add 'notechondria.trance-0.com' to ALLOWED_HOSTS.
  • Fix: after env_list, union BACKEND_CUSTOM_DOMAIN and RENDER_EXTERNAL_HOSTNAME into ALLOWED_HOSTS when they're not already present (and when * isn't already in the list, which would allow everything anyway). Platform-provided hostnames are always trusted; the env var can still extend the list with extra allowed hosts. Same union logic applied to CSRF_TRUSTED_ORIGINS so cross-origin POSTs from the frontend to the backend's platform hostname don't 403 on CSRF either. FRONTEND_ORIGIN gets the same treatment so a GitHub Pages frontend talking to a Render/Northflank backend works without manually listing it.

Files Changed

  • VERSION — bumped 0.1.69 → 0.1.70.
  • frontend/editor_app/lib/app_shell.dart — cache client in a late final _client field on _NotechondriaAppState; build() passes the cached reference instead of re-evaluating the ?? expression.
  • frontend/planner_app/lib/app_shell.dart — same.
  • frontend/portal_app/lib/app_shell.dart — same.
  • backend/notechondria/settings.py — union BACKEND_CUSTOM_DOMAIN / RENDER_EXTERNAL_HOSTNAME into ALLOWED_HOSTS (and mirrored for CSRF_TRUSTED_ORIGINS) regardless of whether DJANGO_ALLOWED_HOSTS is explicitly set.
  • docs/versions/0.1.70.md — this file.

Notes

  • The frontend client caching is a straightforward Flutter pattern mistake — easy to reintroduce when someone copy-pastes the client: widget.client ?? HttpNotechondriaClient() idiom from an older commit. The comment block on each file should be loud enough to catch it in review.
  • The ALLOWED_HOSTS union runs after env_list, so operators can still set DJANGO_ALLOWED_HOSTS to a narrow list for defence-in-depth. The guarantee is that BACKEND_CUSTOM_DOMAIN and RENDER_EXTERNAL_HOSTNAME (both set by the operator / platform, not by external traffic) are at least included.
  • If DJANGO_ALLOWED_HOSTS=* (the Northflank sample default), the union is a no-op — * already permits everything. The check avoids appending to an already-permissive list.
  • Frontend requires a rebuild for the fix to take effect. Triggering a tag or a docs-path commit on main will republish the Pages build with the fixed client caching.