Skip to content

Fix #413–#424: tenancy API and Vary, outer-join tenant filter, i18n overrides, admin a11y - #425

Merged
antosubash merged 30 commits into
mainfrom
feat/issue-batch-413-424
Oct 11, 2026
Merged

antosubash merged 30 commits into
mainfrom
feat/issue-batch-413-424

Conversation

@antosubash

@antosubash antosubash commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes twelve issues filed on 2026-10-08 in one batch. Design: docs/superpowers/specs/2026-10-09-issue-batch-413-424-design.md. Plan: docs/superpowers/plans/2026-10-09-issue-batch-413-424.md. #317 is not included: it needs access to the external design file.

Tenancy

Background tasks

Auth

i18n

Build

UI and admin

Deviations from the approved design (all reviewed):

Verification

  • Browser-verified on Postgres at /admin/users/ and the other touched pages, on port 8201 single-tenant and 8202 multi-tenant.
  • Console: no errors, apart from the intentional 404 resource errors.
  • Local CI:
    • lint, ty, Biome, tsc, the 300-line cap and the untranslated-string check
    • Python 3894 passed; JS 523 passed
    • build
    • full e2e suite: 46 passed
  • Postgres suites (db, tenancy, tenants, background_tasks, users) and the migrations round-trip are green.
  • Verification report: https://claude.ai/artifact/RjngNVs4jxB1fYjPiE79dW

QA Report

  • Full QA cycle completed in 2 iterations.
  • 2 bugs found, 2 fixed: the branding preview's foreground ink, and colour-field validation.
  • Categories tested: happy path, form validation, error states and edge cases, plus multi-tenant tenancy (the NativeSelect invite/members pages and Vary headers verbatim).
  • Follow-ups (P3, not addressed here):
    • ?q=%00 in users or background-tasks search returns 500 on Postgres. This predates the branch.
    • /admin/settings/ nests two <main> landmarks.
    • The admin 404's "Go home" goes to /dashboard/.
    • A whitespace-only users search shows "No users match".
  • Full QA report: https://claude.ai/artifact/CSxvHoFHENcyENuDs2zCg8

Report links are Claude Artifacts and start private — the author can share them
from claude.ai/code/artifacts if reviewers need access.

Test plan

  • Sign out, open /admin/settings/, sign in, and confirm you land back on /admin/settings/.
  • Open /admin/users/00000000-0000-0000-0000-000000000000 as an admin and confirm the 404 renders inside the admin sidebar layout.
  • In /admin/branding/, set a light colour (#62B8E2) and confirm the primary buttons and the active sidebar row use dark text.
  • On Postgres, /admin/background-tasks/ loads.
  • CI is green.

Closes #413, closes #414, closes #415, closes #416, closes #417, closes #418, closes #419, closes #420, closes #421, closes #422, closes #423, closes #424

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

…enant (#423, #424)

Also records the resolution's vary on request.state.tenant_vary and makes the claim path vary on Cookie, Authorization.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
A child column used only inside a function (func.count(Child.id)) loses
its ORM annotation, so _plain_tables took the outer-joined child for a
Core table and added WHERE child.tenant_id = :t, turning the LEFT JOIN
into an inner join. Exclude a columns-clause table from the WHERE
candidates only when it is the target of an outer/full join in
_setup_joins whose target is a plain ORM entity carrying loader criteria
(they already sit in ON). Raw Model.__table__ targets keep the WHERE
predicate; strict mode without a tenant still refuses.

Pins the pre-existing Core outer join leak as a strict xfail.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
A FULL join preserves its right side, and an ON predicate never removes a
preserved row: with the child's WHERE gone, other tenants' (and trashed)
child rows came back as unmatched rows. Exempt LEFT outer joins only, and
pin it with FULL-join tests over a non-tenant left model.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
host/locales/overrides/<locale>.json replaces existing keys by full dotted
path after all catalogs; unknown keys are skipped and reported as SM027.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…lter (#417)

.outerjoin(Parent.kids) and .outerjoin(Parent.kids.of_type(Kid)) record the
relationship attribute as the join target, so the ON-covered exclusion never
matched and func.count(Kid.id) still added WHERE kid.tenant_id = :t, dropping
parents without children. Resolve the relationship's target mapper and apply
the existing entity rules (LEFT only, covered class, single table, tenant
bound). secondary relationships and aliased of_type keep their WHERE.

Also: TenancyMode in simple_module_core.tenancy.__all__; mode_for dropped from
simple_module_hosting.tenancy.__all__ (internal to create_app).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…#415)

make doctor rebuilt the catalog from modules + host + ui but not the
framework's own hosting namespace (setup wizard), so every valid hosting.*
host override was reported as SM027. Locate simple_module_hosting/locales via
importlib.util.find_spec (core must not import hosting) and add it like the
running app's build_i18n_registry does.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
attach_session_listeners guarded with event.contains, which SQLAlchemy keys on
id(target): a new session class reusing a garbage-collected class's id read as
already wired and got no tenant stamping, no tenant/soft-delete filter and no
write marker. Guard with a marker in the class's own __dict__ instead. Found
as an order-dependent test failure (a request's write never committed) that
the new relationship outer-join tests shifted into view.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
- tenants resolver varies on Cookie whenever a signed-in user shaped the answer
- session middleware no longer emits a duplicate Cookie in Vary
- session listeners: a subclass of a wired class inherits its listeners once
- doctor reports malformed override JSON as SM016 instead of crashing
- gen-pages prunes node_modules/__pycache__ during the @source walk
- settings: group headings are h3 under the module h2

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-09T10:24:13.440121Z 6961aee PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6961aee
Status: ✅  Deploy successful!
Preview URL: https://28671ac3.simple-module-python.pages.dev
Branch Preview URL: https://feat-issue-batch-413-424.simple-module-python.pages.dev

View logs

@antosubash
antosubash merged commit 3b4415d into main Oct 11, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment