Skip to content

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

Open
antosubash wants to merge 30 commits into
mainfrom
feat/issue-batch-413-424
Open

antosubash wants to merge 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment