@micha.bigler/ui-core-micha 3.11.0 → 3.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/WORK_ORDERS.md +2 -2
- package/dist/components/QrSignupManager.js +1 -1
- package/dist/components/UserListComponent.js +9 -1
- package/dist/i18n/authTranslations.js +12 -0
- package/docs/DARK-TOKENS.md +1 -1
- package/package.json +1 -1
- package/src/components/QrSignupManager.jsx +1 -1
- package/src/components/UserListComponent.jsx +12 -0
- package/src/i18n/authTranslations.ts +12 -0
- package/tests/QrSignupManagerInfoBox.test.jsx +49 -0
- package/tests/UserListComponent.test.jsx +24 -1
- package/tests/authTranslations.test.js +22 -0
- package/tests/userScreenTranslations.test.jsx +53 -0
- package/work-orders/UCM-I18N-5.md +135 -4
- package/work-orders/UCM-THEME-16.md +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.11.2 — UCM-THEME-16
|
|
4
|
+
|
|
5
|
+
The "Signup Access / valid until" box in `QrSignupManager` now takes `background.subtle` instead of the fixed light `grey.50`,
|
|
6
|
+
so its text stays readable in dark mode (2.00:1 before). In light the ground moves from `#FAFAFA` to `#F4F5F6` (1.05:1 step).
|
|
7
|
+
The white ground behind the QR code itself is unchanged on purpose.
|
|
8
|
+
|
|
9
|
+
## 3.11.1 — UCM-I18N-5
|
|
10
|
+
|
|
11
|
+
`UserListComponent`'s pagination range text ("1–4 of 4") now goes through `labelDisplayedRows`,
|
|
12
|
+
translated in all four supported locales (de/fr/en/sw), including the "more than" wording for an
|
|
13
|
+
unbounded count. Previously only `labelRowsPerPage` was translated, so MUI's English default leaked
|
|
14
|
+
through regardless of locale.
|
|
15
|
+
|
|
3
16
|
## 3.11.0 — UCM-THEME-14
|
|
4
17
|
|
|
5
18
|
Added dark-mode composition to `createAppTheme` from the central dark token baseline, including own-surface
|
package/WORK_ORDERS.md
CHANGED
|
@@ -28,7 +28,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
28
28
|
|
|
29
29
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
30
30
|
|---|---|---|---|---|---|---|
|
|
31
|
-
| UCM-I18N-5 | [User-list pagination range text is hard-coded English](work-orders/UCM-I18N-5.md) | `UserListComponent`'s `TablePagination` sets `labelRowsPerPage` but no `labelDisplayedRows`, so German shows "1–4 of 4". | 2026-10-01 |
|
|
31
|
+
| UCM-I18N-5 | [User-list pagination range text is hard-coded English](work-orders/UCM-I18N-5.md) | `UserListComponent`'s `TablePagination` sets `labelRowsPerPage` but no `labelDisplayedRows`, so German shows "1–4 of 4". | 2026-10-01 | in-progress | | Found in jg-ferien's rendered check 2026-10-01. Follows `UCM-I18N-4`. Tier 3. Done = released AND jg-ferien pin bumped. Envelope written 2026-10-01; operator: hand to the jg Paket-4 session. Implementation map filled 2026-10-02 (`a732025`): the translation bundle carries FOUR locales (de/fr/en/sw), not three — confirmed by reading `authTranslations.ts` directly; proposed two keys (exact-count + "more than" form) with a best-effort, not native-verified, Swahili wording. Dispatching codex/gpt-5.6-luna. |
|
|
32
32
|
| UCM-TEST-1 | A kit integration check every app runs in its own suite | Exported `checkKitIntegration({ i18n, wrapper, pages })` -> `{ findings }` (shape of `assertThemeComplete`): kit keys present in the app's i18n, one `i18next` instance, kit pages render in the app's providers without raw keys, the `invalid`-email case shows the field text. Replaces per-app hand-written wiring tests and manual auth walks. Full order [`work-orders/UCM-TEST-1.md`](work-orders/UCM-TEST-1.md) | 2026-10-01 | done | 2cde120 | Tier 3 (shared-core, auth surface) · operator decision 2026-10-01: central check, apps adopt with their next bump · companion: `workflow-templates` WFT-CI-31. Codex implemented (available today, no fallback). Orchestrator independently found + fixed before review: a false-positive in the raw-code render heuristic (a legitimate short lowercase kit-shipped value, e.g. German `"oder"` for `Auth.LOGIN_OR`, would otherwise be flagged as a leaked backend code — added a `KIT_VALUES` exemption + a pinning test), and independently proved Required Test 5 (resolver-bypassed -> error-text finding) via a transient mutation of `resolveErrorText.js`, restored clean. Independent `reviewer` (all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 9 raised · 6 accepted · worst accepted: blocker**. Accepted and fixed: catalogue check didn't force `fallbackLng: false` per lookup, so a consumer's own `fallbackLng` config (survey_app's is `"de"`, not `"false"`) could silently resolve a missing-language key through the fallback and mask it (tests lens) — fixed, plus a dedicated test using a real fallback-configured instance; Required Test 6 only checked adapter reference-identity, not that a real post-check request actually reaches the real adapter (envelope lens) — strengthened with a sentinel-adapter test; React 19's `act()` needs `IS_REACT_ACT_ENVIRONMENT`, undocumented for consumers (ui lens) — added to the README example; README's `pages` description implied it fully replaces the default set, when `LoginPage` always additionally renders for the error-text check regardless (ui lens, nit) — clarified. Accepted, documented rather than redesigned: mutating the shared `apiClient.defaults.adapter` for the check's duration is process-wide and could intercept a concurrent real request from other code in the same test file (regression lens, blocker) — this is the Envelope's own decided stub design (`src/utils/auth-errors.js`-style module-singleton stubbing, same pattern already used throughout this suite), not fixable without abandoning that decision; added a README caveat instead of restructuring. Rejected: `RAW_CODE` only matches lowercase-snake-case, missing uppercase/hyphenated shapes (envelope + tests lenses, same point from both) — every real backend error code in this codebase (`src/utils/auth-errors.js`, allauth headless responses) is lowercase snake_case; widening the regex matches no real shape here and raises false-positive risk for marginal benefit. `KIT_VALUES` could let a leaked code evade detection if it coincidentally equals a real kit translation value (tests lens) — true but reintroducing the "oder" false positive to close it is a worse trade; no real backend code collides with the kit's UI copy in practice. The correctly-wired/legitimate-word/resolver/adapter-restore tests would all pass against a hardcoded `{findings:[]}` stub in isolation (tests lens, nit) — true of any happy-path assertion; each is paired with a failure-case test proving the same code path actually detects the defect (Required Tests 2-5), which is where the real proof lives. `duplication`: no capability-level overlap with `assertThemeComplete` or any shared-core/app module. **Verification** (WO's own instruction): ran `checkKitIntegration` once against survey_app's real wiring — not by touching that repo, but from inside this repo's own resolved environment, replicating `survey_app/frontend/i18n/index.js`'s real merge + config exactly (`fallbackLng: "de"`, survey_app's actual `translations.json`) plus this repo's real `AuthProvider` — **result: `findings: []`**, including against the real fallback-configured instance the tests-lens finding above was about. Orchestrator's own scoped run: `tests/checkKitIntegration.test.jsx` + `tests/resolveErrorText.test.js` (12 tests) + 7 directly-dependent files (33 tests) = 45/45 green via `npx vitest run <files> --pool=threads --maxWorkers=1`. No rendered gate — library-only change, no running app to preview. **Released as `3.10.0`** (minor — new exported capability, per the WO's own instruction). Per-app adoption rows are added by the Expertenchat when each app's next bump is scoped — not part of this WO. |
|
|
33
33
|
| UCM-AUTH-11 | [`UserListComponent` offers an admin the delete action on their own row](work-orders/UCM-AUTH-11.md) | Without a `canDeleteUser` prop, `canDelete` falls back to `canEdit`, which is true for a superuser or role `admin` on EVERY row, their own included (`src/components/UserListComponent.jsx` `defaultCanEdit`/`canDelete`, ~l.128-156); the backend (dcm) still allows an admin's generic self-delete. Default should never offer deleting one's own row. | 2026-09-30 | released | 83bac37 | From django-core-micha `DCM-AUTH-4` (dropped there): operator decision 2026-09-30, UI-only, backend unchanged. Affected consumers measured: kerzenziehen and reimbursements render ucm's `AccountPage` without props (jg-ferien passes its own `userListCanDeleteUser` and already excludes itself). Both apps pin ucm **2.41.3**, so reaching them needs a major bump (3.0.0 removed chart layout props) — tracked as its own follow-up, not this WO. Gate #1 approved 2026-09-30 (operator handover). Codex implemented (available today, no fallback): new `isOwnRow(targetUser)` helper (`String(id) === String(id)`, `false` whenever either id is unresolved — covers the loading-state risk) gates the delete button's RENDER — `{showDeleteAction && !isOwnRow(row) && (...)}` — not its `disabled` state, so a consumer's `canDeleteUser` returning `true` for the own row cannot bring the button back; `canDelete`/`canEdit`/`defaultCanEdit` untouched. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 3 raised · 1 accepted · worst accepted: nit**. Accepted (nit, tests lens): the committed fixtures only ever used numeric ids for both `currentUser` and the own row, so a strict-equality implementation would have passed every committed test despite violating the Envelope's own `3`/`"3"` identity requirement — added a fifth case with a string-vs-number id mismatch. Rejected: envelope lens's claim that "Required Test 5 (mutation proof) is missing" — the WO's own Part B (mirroring `UCM-THEME-15`/`UCM-PRIM-2`'s established pattern in this repo) asks for a TRANSIENT demonstration during implementation, not a permanent fifth test case; the implementer's own `PROGRESS` narration confirms it removed the gate, watched tests 1 and 3 fail (2 delete buttons found instead of 1), then restored — exactly as asked. Envelope lens's claim that "package.json remains 3.9.0 so the release is incomplete" — true at the moment the reviewer read the diff (version bump + register are the Orchestrator's own finalize step, explicitly forbidden to the implementer), not a defect in the change itself; done now. `regression`/`duplication`/`ui`/`sec` lenses: no findings — `sec_reviewer` specifically confirmed the render-gate cannot be reopened via `canDeleteUser`/`canEditUser`/`extraRowActions`, and that the backend's own continued refusal of an admin's self-delete (`DCM-AUTH-4`) means this UI-only fix is not masking a still-open server-side gap. Orchestrator's own scoped run: `tests/UserListComponent.test.jsx` 7/7 green + `tests/userScreenTranslations.test.jsx` (directly-dependent, also renders this component) 4/4 green, via `npx vitest run <files> --pool=threads --maxWorkers=1` (default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue). No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.9.1`** (patch — behaviour fix of an existing component, no API change, per the WO's own instruction). `npm view @micha.bigler/ui-core-micha version` will read `3.9.1` once the registry's version-list index catches up to the just-completed publish (provenance-signed publishes report success immediately but can take a few minutes to appear in `npm view`/the registry API — confirmed via the GitHub Actions job log on the prior `UCM-AUTH-10` release, not re-verified here to avoid the same registry-lag false alarm). **Not done in this session** (per the WO's own Part C): registering the kerzenziehen/reimbursements major-bump follow-up rows in their own `WORK_ORDERS.md` registers — flagged for whoever next works in those repos. |
|
|
34
34
|
| UCM-AUTH-10 | Error texts never show a raw backend code | One resolver for every kit error display: backend `i18nKey` > field key `Auth.field.<field>.<code>` > code key (AUTH-2) > backend message > caller default; `apiClient` sends the UI language as `Accept-Language`; `invalid`/`required` field keys for every submitted field; ~40 call sites migrated; reconstructs the missing 3.7.0 changelog entry. Full order [`work-orders/UCM-AUTH-10.md`](work-orders/UCM-AUTH-10.md) | 2026-09-30 | done | 9019d3d | Tier 3 (shared-core, auth screens, header on every request) · operator decision 2026-09-30: code-based UI, backend message as last specific fallback, UI language sent to the backend · found in `survey_app` SVA-DEP-1 acceptance (pre-existing, not a bump regression). Codex implemented (available today, no fallback): new `src/utils/resolveErrorText.js` (plain function, not a hook — called inside `catch` blocks) implementing steps 0-4 via `i18n.exists`/`i18n.t`; `extractErrorInfo`/`normaliseApiError` now carry `field` (`param` allauth / `field` dcm, normalised to one name), `i18nKey`, and `backendMessage` (never falls back to the code — the literal bug) as separate fields; `apiClient` gained a request interceptor sending `Accept-Language: i18next.language` only when `i18next.isInitialized` (never guesses); ~40 call sites across 22 files migrated to the uniform pattern (state now always holds resolved display text, never a bare key; render sites dropped their `t()` wrapper); 48 new `Auth.field.<field>.{invalid,required}` keys (24 fields, enumerated from `authApi.jsx`, all 4 locales + `de_informal` where wording differs); `CHANGELOG.md`'s missing `3.7.0` entry reconstructed. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 12 raised · 4 accepted · worst accepted: defect**. Accepted: (defect, envelope lens — a real guard blind spot) `MFAComponent.jsx`'s `setErrorKey('Auth.MFA_TOTP_ALREADY_ACTIVE')` was a plain literal-key setter the mechanical sweep missed — the call-site-guard test only matches the `.code` pattern, not "any literal setter needs `t()` now that render dropped its wrapper"; would have displayed the raw key on screen, exactly the bug this WO exists to fix. Fixed (wrapped in `t()`); no dedicated test exists for this component, fix verified by direct code inspection only. (nit, ui_reviewer) the new field-key catalogue omitted `RegistrationMethodsManager.jsx`'s 5 boolean policy-toggle fields (`allow_admin_invite`, `allow_self_signup_{access_code,open,email_domain,qr}`); added, all 4 locales. (nit, tests lens) `resolveErrorText.test.js`'s "dcm field shape" case was byte-identical to the allauth-shape case, testing nothing new (the param/field normalisation it claimed to cover already happens one layer down, in `extractErrorInfo`, and is genuinely tested there); removed the redundant duplicate. Rejected: 2 lenses (envelope, ui_reviewer) claimed `SupportRecoveryRequestsTab.jsx` calls `resolveErrorText(i18n, …)` without destructuring `i18n` — verified directly against the file twice, `const { t, i18n } = useTranslation()` is present at line 42 and every call site is a closure over it; a stale/misread finding from both lenses, not a real defect. 3 lenses (envelope, regression, tests) flagged step 3 (`backendMessage` rendered verbatim) as an exposure risk — this is the Envelope's own **decided** design (`Operator decision: … the backend message as the last specific fallback`), not an implementation defect; `sec_reviewer` — the lens specifically briefed with dcm's S4 audit-log rationale and tasked with exactly this question — found no HTML-injection path and confirmed React's text-node escaping covers every render site, which is what the Envelope's own risk-review request was actually asking. `sec_reviewer` additionally flagged `BulkInviteCsvTab.jsx:88`'s `err.message` and `PasswordInvitePage.jsx:117`'s `err.raw.messages` as chain bypasses — the former is a purely local CSV-file-read error with no backend response involved at all (confirmed no `apiClient` call in that `try` block), the latter is `UCM-AUTH-9`'s already-shipped, already-reviewed password-rejection-reason display, unchanged by this WO and out of scope per its own non-goal ("no change to which errors are shown, where, or what triggers one"). `envelope`+`tests` lenses' claim that `auth-errors.js`'s native `.message` still falls back to the code — deliberate, per this WO's own Part B ("you may leave `err.message` with its existing fallback-to-code behaviour untouched") and covered by the implementer's own test; the field the resolver actually reads (`backendMessage`) correctly never falls back, which is what the Envelope's separation requirement is about. Orchestrator's own scoped run: 14 files, 102 tests green (`resolveErrorText`/`loginPageErrorResolution`/`errorCallSiteGuard`/`apiClientLanguage`/`authErrors`/`AccountPage`/`PasswordInvitePage`/`SignUpPage`/`UserListComponent`/`QrSignupManager.registrationContext`/`i18nAggregate`/`authTranslations`/`themeCompleteness`/`i18nGermanVariant`), via `npx vitest run <files> --pool=threads --maxWorkers=1` (the default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue; the threads pool is stable). **Verification (the operator's exact screenshot case, rendered end to end):** `en` → "Enter a valid email address."; `de` → "Bitte geben Sie eine gültige E-Mail-Adresse ein." — both resolve via step 1 (`Auth.field.email.invalid`), never the raw `invalid` code, confirmed by `tests/loginPageErrorResolution.test.jsx` (`en`) and by the locale-agnostic resolver design (same key, `de` value) for `de`. No rendered browser gate — verified via the real `LoginPage` component rendered against a real i18next instance in the test harness. **Released as `3.9.0`** (minor, per the Envelope's own instruction — `resolveErrorText` is internal-only, not part of the public `src/index.js` surface). `npm view @micha.bigler/ui-core-micha version` should read `3.9.0` once `publish.yml` runs on this push. No consumer bump in this session (non-goal). |
|
|
@@ -38,7 +38,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
38
38
|
| UCM-MSG-20 | [Message composer collapses on narrow screens](work-orders/UCM-MSG-20.md) | At 375 px with four actions the `Composer` textarea is 16 px wide and Send is clipped; plus linkified URLs (http/https only) and an explained or hidden read marker in `MessageBubble`. | 2026-09-29 | done | fcd4ae6 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-UX-14`. dcm register is canonical for `MSG-*`; this is the ucm-side row. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback): `Composer`'s action row split into an icon row + a message-field/send row (`sx={{flex:1,minWidth:120}}`/`sx={{flexShrink:0}}`); new `linkifyText.jsx` (strict `https?://` inclusion pattern, trims trailing sentence punctuation); `ReadTicks` wrapped in a `Tooltip` and hidden entirely when `recipient_count === 0`. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 6 raised · 3 accepted · worst accepted: defect** — the composer test only asserted CSS values, not that the actions actually moved to a separate DOM row (a same-row implementation with the same sx values would have passed); the read-marker/linkify test suite lacked a structural row-separation proof; and `linkifyText`'s raw `<a>` diverged from the kit's own `Link` primitive already used elsewhere (`CookieConsentStep.jsx`). Fixed by the Orchestrator: added a structural assertion (attach button's `Stack` ancestor does not contain the message field or send button); swapped `<a>` for MUI `Link` (which then broke `.closest('.MuiTypography-root')` in the punctuation test — Link itself carries that class — fixed to `.closest('p')`); **also found and fixed, not from a reviewer but from the Orchestrator's own test run:** the Composer's two new inline `sx` objects tripped `reportKitSxBypasses` (a pre-existing repo-wide guard neither the implementer nor any reviewer lens checked) — extracted to exported `COMPOSER_MESSAGE_FIELD_SX`/`COMPOSER_SEND_BUTTON_SX` with new `kitSxRegistry.js` entries. Rejected: "actions row split at every width, not just narrow" (Envelope's own wording allows "wrap to their own row" unconditionally, and a width-gated variant would need media queries for no stated benefit); the `Link`-vs-`<a>` and `minWidth: 120`-magic-number nits beyond what's already fixed above (the latter matches this repo's own `StatTile.jsx:10` `minWidth = 120` convention, not a violation); sec_reviewer found no issues (clean). Orchestrator's own scoped run: 14/14 new/extended (`messagingComposer`/`messagingLinkify`/`messagingReadTicksTooltip`) + `themeCompleteness` 38/38 (sx-registry re-check) + 16/16 directly-dependent regression (`messagingCompactBubble`/`messagingChunk6`/`messagingComposerChunk5`), all via `npx vitest run <files> --maxWorkers=1`. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `PRIM-2`/`SHELL-7`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note; jg-ferien's pin bump is that app's own separate step. |
|
|
39
39
|
| UCM-SHELL-7 | [User-menu avatar initials fail contrast](work-orders/UCM-SHELL-7.md) | `UserMenu` avatar renders MUI default white-on-grey initials at 1.83:1; take colours from theme tokens with a passing fallback. | 2026-09-29 | done | 04a29fe | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-DL-12`. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback): avatar background = `primary.main`, text dynamically picked between white/black by measured contrast (`calculateContrastRatio`, already exported). Independent `ui_reviewer` only (Codex gpt-5.6-luna) — Envelope scopes this to ui_reviewer alone, no new logic beyond a colour choice: **review: codex/gpt-5.6-luna · lenses: ui_reviewer · 1 raised · 1 accepted · worst accepted: defect** — the implementer's fallback used `ink.primary` (#212529, luminance ~0.018) instead of true black, which is NOT mathematically guaranteed to reach 4.5:1 together with `background.paper`: for a `primary.main` in the ~0.183–0.257 luminance band neither branch cleared 4.5:1, and neither required test exercised that fallback branch to catch it. Fixed by the Orchestrator: switched both branches to `theme.palette.common.white`/`.black` (true white/black, always present on any MUI theme, not a source literal) — contrast-against-true-white × contrast-against-true-black is a luminance-independent constant (21), so `max(...) >= sqrt(21) ~= 4.58` holds for every possible `primary.main`, closing the gap rather than narrowing it. Added a third test with a synthetic mid-grey `primary.main` (`#808080`, ~3.95:1 on white) to actually exercise the black-fallback branch. Orchestrator's own scoped run: `tests/UserMenu.test.jsx` 10/10 green (`npx vitest run tests/UserMenu.test.jsx --maxWorkers=1`). No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `PRIM-2`/`MSG-20`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note; jg-ferien's pin bump is that app's own separate step. |
|
|
40
40
|
| UCM-PRIM-2 | [Shared EmptyState primitive](work-orders/UCM-PRIM-2.md) | New `EmptyState` export (icon, one line, optional secondary line and action) from theme tokens; jg-ferien shows empty sections in seven different ways today. | 2026-09-29 | done | 059e9f0 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. Adopted in jg `JG-UX-9`. Tier 3 (new public export, additive). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback). Independent `reviewer` (Codex gpt-5.6-luna, all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna), concurrent background batch on the full diff: **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 5 raised · 3 accepted · worst accepted: defect** — missing README doc entry (Envelope required one, added), action button missing `type="button"` (form-submit risk, fixed), two test assertions passed for a would-be hardcoded stub (title/description always rendered the same fixed string across all 3 tests; strengthened with a second distinct-value render + negative assertion). Rejected: `sx` passthrough flagged as an "unrequested escape hatch" (established kit convention, e.g. `Composer`'s emoji button, not a new pattern); colour-guard/action-callback "could pass under a stub" claims (the callback test uses a distinct mock reference, which a hardcoded stub cannot satisfy; the colour guard test does exactly what Required Test #2 asked). Orchestrator's own scoped run: `tests/EmptyState.test.jsx` 4/4 green, `tests/themeCompleteness.test.js` (directly-dependent, directory-enumerated palette scan) 38/38 green. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `SHELL-7`/`MSG-20`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note. |
|
|
41
|
-
| UCM-THEME-16 | Dark-safe
|
|
41
|
+
| UCM-THEME-16 | Dark-safe info box in `QrSignupManager` | `QrSignupManager.jsx:376` info box `grey.50` -> `background.subtle` (dark text was 2.00:1 on the fixed light ground). Operator decision 2026-10-02: the small light change `#FAFAFA` -> `#F4F5F6` is accepted; the QR code's own white ground (`:361`) and `ReadTicks.jsx` `grey.500` (6.07:1 in dark) stay unchanged on purpose. Full order [`work-orders/UCM-THEME-16.md`](work-orders/UCM-THEME-16.md) | 2026-10-02 | done | 7834231 | Tier 3 (change inside shared-core; corrected from Tier 2) · reviewer + ui_reviewer · patch release · raised by the UCM-THEME-14 review - 2026-10-02: Orchestrator implemented (one line, authorship with the Claude orchestrator; Codex not needed) - review: codex/gpt-5.6-luna - lenses: envelope,regression,duplication,tests + ui_review - 2 raised - 1 accepted - worst accepted: nit (hex in test assertion removed; rejected: apps without background.subtle, MFAComponent already depends on it) - release 3.11.2 - test fails on revert to grey.50 (mutation-checked) - no rendered side-by-side (no prototype; style assertion in both modes) |
|
|
42
42
|
| UCM-THEME-14 | Dark mode in `createAppTheme`, from the central token file | `palette.mode: 'dark'` selects a dark baseline palette in `tokens.js`; focus derivation and completeness contrast checks judge a theme against its own surfaces instead of a fixed white; light output unchanged (regression guard). Full order [`work-orders/UCM-THEME-14.md`](work-orders/UCM-THEME-14.md) | 2026-09-29 | done | ddecdba | Tier 3 (inside shared-core) - Gate #1 carried by the committed Envelope + mini-handover - Codex implemented (gpt-5.6-luna, available today, no fallback); orchestrator fixed review findings (reverted two grey.* token swaps that changed light output, rgb() ink support, primary light/dark shades, test hardening, 0.02 alpha step to reproduce the frozen 0.38/0.53) - review: codex/gpt-5.6-luna - lenses: envelope,regression,duplication,tests + ui_review - 13 raised - 6 accepted - worst accepted: defect - light guard fixture proven against HEAD src - rendered check: dev harness Theme/Shared baseline, dark at 375 px and 1280 px (live browser screenshots; light not re-rendered, covered by the fixture guard) - full kit suite run once incidentally: 722 pass, 2 flaky under parallel load (messagingComposerChunk5, packageTreeShaking) pass in isolation - release 3.11.0 publishes on push - downstream: webshop-guenter pin bump planned
|
|
43
43
|
| UCM-THEME-13 | Freeze a dark-mode baseline token set | Reference sheet `docs/DARK-TOKENS.md` with a decided value per mode-dependent token class plus a contrast table; `design-language` process, operator-led, no code. Full order [`work-orders/UCM-THEME-13.md`](work-orders/UCM-THEME-13.md) | 2026-09-29 | done | 7330f91 | Tier 2 (document only) · operator decision 2026-09-29: dark mode gets its own kit order · only consumer today: `webshop-guenter` · **Frozen 2026-10-02 by the operator** after two value rounds (neutral base A = estate convergence `#14181B`; surface step 1 = estate's own). Sheet `docs/DARK-TOKENS.md`, instrument `work-orders/assets/UCM-THEME-13-dark-prototype.html`. Invented and labelled: dark `info`, dark `stale`, overlay shadow, scrim. Document order: no implementer, no review (Envelope). |
|
|
44
44
|
| AUTH-7 | `loginWithPassword`s 409-Retry gegen Zweitfehlschlag haerten | allauth-headless antwortet mit 409 ("already authenticated"), wenn die Session bereits gueltig ist; `loginWithPassword` ruft dann `fetchCurrentUser()` erneut auf. Schlaegt dieser Retry ebenfalls fehl, wurde der Fehler bisher roh durchgereicht statt normalisiert. Spec [`work-orders/AUTH-7.md`](work-orders/AUTH-7.md) | 2026-09-05 | done | ae7aa89 | **Tier 3** (Shared-Core). Ausgeloest durch `kira`: ein erschoepfter globaler Throttle-Bucket (siehe `kira/KIRA-TRACK-3`) liess den 409-Retry selbst mit 429 fehlschlagen, roh statt normalisiert durchgereicht. Rein additiver Bugfix, kein Prop-/Verhaltensvertrag geaendert. **Implementiert (Codex gpt-5.6-luna):** genau wie geplant, `src/auth/authApi.jsx` + neue `tests/authApiLogin.test.js`. review: codex/gpt-5.6-luna (`reviewer` 4 Lenses + `ui_reviewer` + `sec_reviewer`, ein Hintergrund-Batch) 3 raised · 1 accepted · 2 rejected · worst accepted: defect (aber ausserhalb des Scopes dieser WO, siehe unten) — `envelope`/`regression`/`duplication`/`sec_reviewer`: 0 raised. `tests`-Lens: 2 raised, BEIDE empirisch widerlegt (Fix testweise zurueckgerollt, betroffener Test schlug korrekt fehl — sind keine echten Luecken) → rejected. `ui_reviewer`: 1 raised, akzeptiert als real (empirisch bestaetigt: `normaliseApiError('...', 'Auth.LOGIN_FAILED')` liefert bei einem DRF-`{"detail": "..."}`-Fehler `code: 'GENERIC'` statt des übergebenen Default-Codes, weil `extractErrorInfo` in `src/utils/auth-errors.js` `'GENERIC'` bereits als gesetzten Wert zurückgibt, der `info.code || defaultCode` gewinnt) — **PRE-EXISTING in einer anderen Datei, nicht durch diesen Diff verursacht, an JEDER anderen Call-Site derselben Funktion bereits vorhanden** → out-of-scope fuer AUTH-7, nicht hier gefixt, nachverfolgt als `AUTH-8`. Tests: `npx vitest run tests/authApiLogin.test.js tests/AuthContext.test.jsx tests/authApiRegistration.test.js` 7/7 gruen. **PATCH-Bump 3.5.0 -> 3.5.1** (`package.json` + `CHANGELOG.md`), kein Consumer-Pin-Bump noetig (rein additiv). |
|
|
@@ -289,7 +289,7 @@ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_D
|
|
|
289
289
|
mt: 2,
|
|
290
290
|
borderRadius: 3,
|
|
291
291
|
p: 2,
|
|
292
|
-
bgcolor: '
|
|
292
|
+
bgcolor: 'background.subtle',
|
|
293
293
|
border: '1px solid',
|
|
294
294
|
borderColor: 'divider',
|
|
295
295
|
}, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('Auth.SIGNUP_QR_ACCESS_TITLE', 'Signup Access') }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'), ": ", formattedExpiry || result.expires_at] })] }), _jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, sx: { mt: 2 }, children: [_jsx(Button, { variant: "outlined", onClick: () => generate(), disabled: busy, children: t('Auth.SIGNUP_QR_NEW_BUTTON', 'New QR-Code') }), _jsx(Button, { variant: "outlined", onClick: handleCopyLink, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_COPY_BUTTON', 'Copy Link') }), _jsx(Button, { variant: "outlined", onClick: handleSavePdf, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_PDF_BUTTON', 'Save as PDF') })] })] }))] }));
|
|
@@ -13,6 +13,14 @@ export const USER_LIST_ALERT_SX = { mb: 2 };
|
|
|
13
13
|
export const USER_LIST_HEADER_CELL_SX = { fontWeight: 600, whiteSpace: 'nowrap' };
|
|
14
14
|
export const USER_LIST_EMPTY_CELL_SX = { py: 4, color: 'text.secondary' };
|
|
15
15
|
export const USER_LIST_BODY_CELL_SX = { verticalAlign: 'top', py: 1 };
|
|
16
|
+
// Extracted so the count === -1 ("more than") branch is directly unit-testable -- the live
|
|
17
|
+
// TablePagination call site always passes a real count from sortedUsers.length, so that branch
|
|
18
|
+
// can never actually be exercised through a render of this component itself (UCM-I18N-5 review).
|
|
19
|
+
export function formatDisplayedRowsLabel(t) {
|
|
20
|
+
return ({ from, to, count }) => (count === -1
|
|
21
|
+
? t('UserList.DISPLAYED_ROWS_MORE_THAN', '{{from}}–{{to}} of more than {{to}}', { from, to })
|
|
22
|
+
: t('UserList.DISPLAYED_ROWS', '{{from}}–{{to}} of {{count}}', { from, to, count }));
|
|
23
|
+
}
|
|
16
24
|
export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, showNewColumn = true, showSuccessfulLoginColumn = true, showRoleColumn = true, onChangeRole = null, showDeleteAction = true, canDeleteUser = null, onDeleteUser = null, }) {
|
|
17
25
|
const { t, i18n } = useTranslation();
|
|
18
26
|
const [users, setUsers] = useState([]);
|
|
@@ -383,5 +391,5 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
383
391
|
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: USER_LIST_ALERT_SX, children: error }), loading && (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })), !loading && (_jsxs(Box, { sx: { width: '100%' }, children: [_jsx(TableContainer, { sx: { border: 1, borderColor: 'divider', borderRadius: 1 }, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { sx: { bgcolor: 'action.hover' }, children: _jsx(TableRow, { children: columns.map((col) => (_jsx(TableCell, { align: col.headerAlign || col.align || 'left', sortDirection: sortField === col.field ? sortDir : false, sx: [USER_LIST_HEADER_CELL_SX, { minWidth: col.minWidth }], children: col.sortable !== false ? (_jsx(TableSortLabel, { active: sortField === col.field, direction: sortField === col.field ? sortDir : 'asc', onClick: () => handleSortClick(col.field), children: col.headerName })) : (col.headerName) }, col.field))) }) }), _jsx(TableBody, { children: pagedUsers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length, align: "center", sx: USER_LIST_EMPTY_CELL_SX, children: t('UserList.NO_USERS', 'No users found.') }) })) : (pagedUsers.map((row) => (_jsx(TableRow, { hover: true, children: columns.map((col) => (_jsx(TableCell, { align: col.align || 'left', sx: USER_LIST_BODY_CELL_SX, children: col.renderCell(row) }, col.field))) }, row.id)))) })] }) }), _jsx(TablePagination, { component: "div", count: sortedUsers.length, page: page, onPageChange: (_, newPage) => setPage(newPage), rowsPerPage: pageSize, onRowsPerPageChange: (event) => {
|
|
384
392
|
setPageSize(Number(event.target.value));
|
|
385
393
|
setPage(0);
|
|
386
|
-
}, labelRowsPerPage: t('UserList.ROWS_PER_PAGE', 'Rows per page:'), rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));
|
|
394
|
+
}, labelRowsPerPage: t('UserList.ROWS_PER_PAGE', 'Rows per page:'), labelDisplayedRows: formatDisplayedRowsLabel(t), rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));
|
|
387
395
|
}
|
|
@@ -2000,6 +2000,18 @@ export const authTranslations = {
|
|
|
2000
2000
|
"en": "Rows per page:",
|
|
2001
2001
|
"sw": "Safu kwa ukurasa:"
|
|
2002
2002
|
},
|
|
2003
|
+
"UserList.DISPLAYED_ROWS": {
|
|
2004
|
+
"de": "{{from}}–{{to}} von {{count}}",
|
|
2005
|
+
"fr": "{{from}}–{{to}} sur {{count}}",
|
|
2006
|
+
"en": "{{from}}–{{to}} of {{count}}",
|
|
2007
|
+
"sw": "{{from}}–{{to}} ya {{count}}"
|
|
2008
|
+
},
|
|
2009
|
+
"UserList.DISPLAYED_ROWS_MORE_THAN": {
|
|
2010
|
+
"de": "{{from}}–{{to}} von mehr als {{to}}",
|
|
2011
|
+
"fr": "{{from}}–{{to}} sur plus de {{to}}",
|
|
2012
|
+
"en": "{{from}}–{{to}} of more than {{to}}",
|
|
2013
|
+
"sw": "{{from}}–{{to}} ya zaidi ya {{to}}"
|
|
2014
|
+
},
|
|
2003
2015
|
"Auth.INVITE_SENT_SUCCESS": {
|
|
2004
2016
|
"de": "Einladung gesendet.",
|
|
2005
2017
|
"fr": "Invitation envoyée.",
|
package/docs/DARK-TOKENS.md
CHANGED
|
@@ -47,7 +47,7 @@ stated rule from a decided value) or **invented** (no precedent anywhere; labell
|
|
|
47
47
|
|
|
48
48
|
| Token | Light (today) | Dark | Origin |
|
|
49
49
|
|---|---|---|---|
|
|
50
|
-
| `controlBorder.main` | `rgba(33,37,41,.50)` | `rgba(230,235,239,.38)` | derived: `ink.primary` at the smallest alpha that clears 3:1 on page and paper |
|
|
50
|
+
| `controlBorder.main` | `rgba(33,37,41,.50)` | `rgba(230,235,239,.38)` | derived: `ink.primary` at the smallest alpha **in 0.02 steps from 0.30** that clears 3:1 on page and paper (wording made precise 2026-10-02; a finer step would give 0.37, the frozen value is 0.38) |
|
|
51
51
|
| `controlBorder.hover` | `rgba(33,37,41,.65)` | `rgba(230,235,239,.53)` | derived: the light step (+0.15) carried over |
|
|
52
52
|
| `controlBorder.error` | `#BF3227` | `#E58B80` | = `error.main` (dark) |
|
|
53
53
|
| `controlBorder.focus` | derived from primary | derived from primary | rule: lighten the primary until it clears 3:1 on page and paper |
|
package/package.json
CHANGED
|
@@ -34,6 +34,17 @@ export const USER_LIST_HEADER_CELL_SX = { fontWeight: 600, whiteSpace: 'nowrap'
|
|
|
34
34
|
export const USER_LIST_EMPTY_CELL_SX = { py: 4, color: 'text.secondary' };
|
|
35
35
|
export const USER_LIST_BODY_CELL_SX = { verticalAlign: 'top', py: 1 };
|
|
36
36
|
|
|
37
|
+
// Extracted so the count === -1 ("more than") branch is directly unit-testable -- the live
|
|
38
|
+
// TablePagination call site always passes a real count from sortedUsers.length, so that branch
|
|
39
|
+
// can never actually be exercised through a render of this component itself (UCM-I18N-5 review).
|
|
40
|
+
export function formatDisplayedRowsLabel(t) {
|
|
41
|
+
return ({ from, to, count }) => (
|
|
42
|
+
count === -1
|
|
43
|
+
? t('UserList.DISPLAYED_ROWS_MORE_THAN', '{{from}}–{{to}} of more than {{to}}', { from, to })
|
|
44
|
+
: t('UserList.DISPLAYED_ROWS', '{{from}}–{{to}} of {{count}}', { from, to, count })
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export function UserListComponent({
|
|
38
49
|
roles = DEFAULT_ROLES,
|
|
39
50
|
currentUser,
|
|
@@ -597,6 +608,7 @@ export function UserListComponent({
|
|
|
597
608
|
setPage(0);
|
|
598
609
|
}}
|
|
599
610
|
labelRowsPerPage={t('UserList.ROWS_PER_PAGE', 'Rows per page:')}
|
|
611
|
+
labelDisplayedRows={formatDisplayedRowsLabel(t)}
|
|
600
612
|
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
601
613
|
/>
|
|
602
614
|
</Box>
|
|
@@ -2053,6 +2053,18 @@ export const authTranslations = {
|
|
|
2053
2053
|
"en": "Rows per page:",
|
|
2054
2054
|
"sw": "Safu kwa ukurasa:"
|
|
2055
2055
|
},
|
|
2056
|
+
"UserList.DISPLAYED_ROWS": {
|
|
2057
|
+
"de": "{{from}}–{{to}} von {{count}}",
|
|
2058
|
+
"fr": "{{from}}–{{to}} sur {{count}}",
|
|
2059
|
+
"en": "{{from}}–{{to}} of {{count}}",
|
|
2060
|
+
"sw": "{{from}}–{{to}} ya {{count}}"
|
|
2061
|
+
},
|
|
2062
|
+
"UserList.DISPLAYED_ROWS_MORE_THAN": {
|
|
2063
|
+
"de": "{{from}}–{{to}} von mehr als {{to}}",
|
|
2064
|
+
"fr": "{{from}}–{{to}} sur plus de {{to}}",
|
|
2065
|
+
"en": "{{from}}–{{to}} of more than {{to}}",
|
|
2066
|
+
"sw": "{{from}}–{{to}} ya zaidi ya {{to}}"
|
|
2067
|
+
},
|
|
2056
2068
|
"Auth.INVITE_SENT_SUCCESS": {
|
|
2057
2069
|
"de": "Einladung gesendet.",
|
|
2058
2070
|
"fr": "Invitation envoyée.",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ThemeProvider } from '@mui/material/styles';
|
|
5
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
vi.mock('../src/auth/authApi', () => ({
|
|
9
|
+
createSignupQr: vi.fn().mockResolvedValue({
|
|
10
|
+
signup_url: 'https://example.test/signup?rt=token',
|
|
11
|
+
expires_at: '2026-09-01T12:00:00Z',
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('qrcode.react', () => ({
|
|
16
|
+
QRCodeSVG: () => null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const stableT = (_key, fallback) => fallback;
|
|
20
|
+
const stableI18n = { exists: () => false, t: stableT };
|
|
21
|
+
vi.mock('react-i18next', () => ({
|
|
22
|
+
useTranslation: () => ({ t: stableT, i18n: stableI18n }),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import { QrSignupManager } from '../src/components/QrSignupManager';
|
|
26
|
+
import { createAppTheme } from '../src/theme';
|
|
27
|
+
|
|
28
|
+
// jsdom serialises the resolved colour as rgb(), so compare against the theme's own value in that form.
|
|
29
|
+
function toRgb(hex) {
|
|
30
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
31
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('QrSignupManager info box', () => {
|
|
35
|
+
afterEach(cleanup);
|
|
36
|
+
|
|
37
|
+
it.each(['light', 'dark'])('paints background.subtle of the %s theme', async (mode) => {
|
|
38
|
+
const theme = createAppTheme({ palette: { mode, primary: { main: '#468AB2' } } });
|
|
39
|
+
render(
|
|
40
|
+
<ThemeProvider theme={theme}>
|
|
41
|
+
<QrSignupManager enabled expiryDays={90} />
|
|
42
|
+
</ThemeProvider>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const title = await screen.findByText('Signup Access');
|
|
46
|
+
const box = title.parentElement;
|
|
47
|
+
expect(getComputedStyle(box).backgroundColor).toBe(toRgb(theme.palette.background.subtle));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -33,7 +33,7 @@ vi.mock('react-i18next', () => {
|
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
import { UserListComponent } from '../src/components/UserListComponent';
|
|
36
|
+
import { UserListComponent, formatDisplayedRowsLabel } from '../src/components/UserListComponent';
|
|
37
37
|
|
|
38
38
|
const currentUser = { id: 99, is_superuser: true, role: 'admin' };
|
|
39
39
|
const user = { id: 1, email: 'person@example.com', role: 'student' };
|
|
@@ -167,3 +167,26 @@ describe('UserListComponent delete actions', () => {
|
|
|
167
167
|
expect(within(getRowByEmail(user.email)).getByRole('button', { name: 'Delete' }).disabled).toBe(false);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
describe('formatDisplayedRowsLabel (UCM-I18N-5)', () => {
|
|
172
|
+
// A real i18next instance's t() supports {{var}} interpolation; this fake mirrors just that,
|
|
173
|
+
// so these assertions pin the KEY and VARS formatDisplayedRowsLabel passes through -- the
|
|
174
|
+
// count === -1 ("more than") branch can never be reached by rendering UserListComponent itself
|
|
175
|
+
// (its own TablePagination always receives a real, non-negative count), so this is the only way
|
|
176
|
+
// to exercise it at all (UCM-I18N-5 review, tests lens).
|
|
177
|
+
function fakeT(key, fallback, vars = {}) {
|
|
178
|
+
const template = { [key]: fallback }[key] ?? fallback;
|
|
179
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, name) => String(vars[name] ?? `{{${name}}}`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
it('uses the exact-count key when count is a real number', () => {
|
|
183
|
+
const label = formatDisplayedRowsLabel(fakeT)({ from: 1, to: 10, count: 42 });
|
|
184
|
+
expect(label).toBe('1–10 of 42');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('uses the "more than" key, without a literal count, when count is -1', () => {
|
|
188
|
+
const label = formatDisplayedRowsLabel(fakeT)({ from: 1, to: 10, count: -1 });
|
|
189
|
+
expect(label).toBe('1–10 of more than 10');
|
|
190
|
+
expect(label).not.toContain('-1');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -50,3 +50,25 @@ describe('authTranslations — AUTH-2 raw backend error codes', () => {
|
|
|
50
50
|
expect(authTranslations['Auth.MFA_AUTHENTICATE_FAILED']).toBeDefined();
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('authTranslations — UserList pagination labels', () => {
|
|
55
|
+
it.each(['UserList.DISPLAYED_ROWS', 'UserList.DISPLAYED_ROWS_MORE_THAN'])('%s has all supported locale entries', (key) => {
|
|
56
|
+
expect(authTranslations[key]).toBeDefined();
|
|
57
|
+
for (const locale of ['de', 'fr', 'en', 'sw']) {
|
|
58
|
+
expect(authTranslations[key][locale]).toEqual(expect.any(String));
|
|
59
|
+
expect(authTranslations[key][locale].trim()).not.toBe('');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// A nonempty string isn't enough -- a locale missing an interpolation placeholder the
|
|
64
|
+
// others carry would silently render the literal "{{count}}" to a real user (UCM-I18N-5 review).
|
|
65
|
+
it.each(['de', 'fr', 'en', 'sw'])('%s has {{from}} and {{to}} in both pagination keys, and {{count}} only in the exact-count one', (locale) => {
|
|
66
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{from}}');
|
|
67
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{to}}');
|
|
68
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{count}}');
|
|
69
|
+
|
|
70
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).toContain('{{from}}');
|
|
71
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).toContain('{{to}}');
|
|
72
|
+
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).not.toContain('{{count}}');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -22,6 +22,9 @@ import { UserListComponent } from '../src/components/UserListComponent';
|
|
|
22
22
|
const germanResources = Object.fromEntries(
|
|
23
23
|
Object.entries(authTranslations).map(([key, value]) => [key, value.de]),
|
|
24
24
|
);
|
|
25
|
+
const englishResources = Object.fromEntries(
|
|
26
|
+
Object.entries(authTranslations).map(([key, value]) => [key, value.en]),
|
|
27
|
+
);
|
|
25
28
|
const i18n = i18next.createInstance();
|
|
26
29
|
i18n.init({
|
|
27
30
|
lng: 'de',
|
|
@@ -31,12 +34,22 @@ i18n.init({
|
|
|
31
34
|
nsSeparator: false,
|
|
32
35
|
interpolation: { escapeValue: false },
|
|
33
36
|
});
|
|
37
|
+
const englishI18n = i18next.createInstance();
|
|
38
|
+
englishI18n.init({
|
|
39
|
+
lng: 'en',
|
|
40
|
+
fallbackLng: 'en',
|
|
41
|
+
resources: { en: { translation: englishResources } },
|
|
42
|
+
keySeparator: false,
|
|
43
|
+
nsSeparator: false,
|
|
44
|
+
interpolation: { escapeValue: false },
|
|
45
|
+
});
|
|
34
46
|
|
|
35
47
|
const ENGLISH_DEFAULTS = [
|
|
36
48
|
'All Users',
|
|
37
49
|
'Search',
|
|
38
50
|
'Search users...',
|
|
39
51
|
'Rows per page:',
|
|
52
|
+
'of 1',
|
|
40
53
|
'Successful Login',
|
|
41
54
|
'Are you sure you want to delete this user?',
|
|
42
55
|
'Invite a new user',
|
|
@@ -51,6 +64,10 @@ function withGermanI18n(children) {
|
|
|
51
64
|
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
function withEnglishI18n(children) {
|
|
68
|
+
return <I18nextProvider i18n={englishI18n}>{children}</I18nextProvider>;
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
function expectNoEnglishDefaults() {
|
|
55
72
|
const text = document.body.textContent;
|
|
56
73
|
for (const english of ENGLISH_DEFAULTS) {
|
|
@@ -112,6 +129,7 @@ describe('German user and invite screen translations', () => {
|
|
|
112
129
|
await waitFor(() => expect(screen.getByText('Alle Benutzer')).toBeTruthy());
|
|
113
130
|
expect(screen.getByLabelText('Suche')).toBeTruthy();
|
|
114
131
|
expect(screen.getByPlaceholderText('Benutzer suchen...')).toBeTruthy();
|
|
132
|
+
expect(screen.getByText('1–1 von 1')).toBeTruthy();
|
|
115
133
|
for (const german of ['Neu', 'Erfolgreiche Anmeldung', 'Rolle', 'Aktionen', 'Zeilen pro Seite:', 'Löschen']) {
|
|
116
134
|
expect(screen.getByText(german)).toBeTruthy();
|
|
117
135
|
}
|
|
@@ -124,6 +142,41 @@ describe('German user and invite screen translations', () => {
|
|
|
124
142
|
expectNoEnglishDefaults();
|
|
125
143
|
});
|
|
126
144
|
|
|
145
|
+
it('renders the pagination range in English when the locale is English', async () => {
|
|
146
|
+
render(withEnglishI18n(<UserListComponent currentUser={{ id: 99, is_superuser: true, role: 'admin' }} />));
|
|
147
|
+
|
|
148
|
+
await waitFor(() => expect(screen.getByText('1–1 of 1')).toBeTruthy());
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('actually goes through the translation key, not a coincidental match with MUI\'s own default', async () => {
|
|
152
|
+
// The real English wording is deliberately identical to MUI's own hardcoded default text
|
|
153
|
+
// (both "{from}-{to} of {count}"), so the assertion above would still pass even if
|
|
154
|
+
// labelDisplayedRows were reverted to unset entirely. Swap in a distinctive marker for just
|
|
155
|
+
// this one key to prove the render genuinely resolves it through i18next, not MUI's fallback
|
|
156
|
+
// (UCM-I18N-5 review, tests lens).
|
|
157
|
+
const markerI18n = i18next.createInstance();
|
|
158
|
+
markerI18n.init({
|
|
159
|
+
lng: 'en',
|
|
160
|
+
fallbackLng: 'en',
|
|
161
|
+
resources: {
|
|
162
|
+
en: {
|
|
163
|
+
translation: { ...englishResources, 'UserList.DISPLAYED_ROWS': '__MARKER__ {{from}}-{{to}}/{{count}}' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
keySeparator: false,
|
|
167
|
+
nsSeparator: false,
|
|
168
|
+
interpolation: { escapeValue: false },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<I18nextProvider i18n={markerI18n}>
|
|
173
|
+
<UserListComponent currentUser={{ id: 99, is_superuser: true, role: 'admin' }} />
|
|
174
|
+
</I18nextProvider>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await waitFor(() => expect(screen.getByText('__MARKER__ 1-1/1')).toBeTruthy());
|
|
178
|
+
});
|
|
179
|
+
|
|
127
180
|
it('renders UserInviteComponent in German and translates the success message', async () => {
|
|
128
181
|
render(withGermanI18n(<UserInviteComponent />));
|
|
129
182
|
|
|
@@ -39,9 +39,106 @@
|
|
|
39
39
|
|
|
40
40
|
# B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
## Context package
|
|
43
|
+
|
|
44
|
+
### The call site
|
|
45
|
+
|
|
46
|
+
`src/components/UserListComponent.jsx:589-601`:
|
|
47
|
+
```jsx
|
|
48
|
+
<TablePagination
|
|
49
|
+
component="div"
|
|
50
|
+
count={sortedUsers.length}
|
|
51
|
+
page={page}
|
|
52
|
+
onPageChange={(_, newPage) => setPage(newPage)}
|
|
53
|
+
rowsPerPage={pageSize}
|
|
54
|
+
onRowsPerPageChange={(event) => {
|
|
55
|
+
setPageSize(Number(event.target.value));
|
|
56
|
+
setPage(0);
|
|
57
|
+
}}
|
|
58
|
+
labelRowsPerPage={t('UserList.ROWS_PER_PAGE', 'Rows per page:')}
|
|
59
|
+
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
`t`/`i18n` come from `useTranslation()` (`react-i18next`), already in scope at l.53.
|
|
63
|
+
|
|
64
|
+
### MUI's `labelDisplayedRows` contract
|
|
65
|
+
|
|
66
|
+
MUI's own default (`node_modules/@mui/material/TablePagination/TablePagination.js:119-124`,
|
|
67
|
+
for reference only — do not import it):
|
|
68
|
+
```js
|
|
69
|
+
function defaultLabelDisplayedRows({ from, to, count }) {
|
|
70
|
+
return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
The prop signature is `({ from, to, count, page }) => ReactNode`. `count` is `-1` when the total
|
|
74
|
+
is unknown/unbounded — this component always passes a real `sortedUsers.length`, but the prop
|
|
75
|
+
contract still has to handle `-1` defensively (the Risk note): MUI itself would call the function
|
|
76
|
+
with `-1` only if `count={-1}` were passed, which this component doesn't do — handle it anyway
|
|
77
|
+
since the function must satisfy the full contract, not just this component's current call site.
|
|
78
|
+
|
|
79
|
+
### The translation bundle has FOUR locales, not three
|
|
80
|
+
|
|
81
|
+
`src/i18n/authTranslations.ts` (flat `export const authTranslations = { "Key.NAME": { de, fr, en,
|
|
82
|
+
sw }, ... }`) carries `de`/`fr`/`en`/`sw` for every existing key, confirmed by grepping all locale
|
|
83
|
+
codes in the file — **sw (Swahili) is real and already translated throughout, not a stub**. "every
|
|
84
|
+
locale the bundles carry" in the Envelope means these four, not jg-ferien's usual three. Add the
|
|
85
|
+
new key(s) in the same shape, next to `UserList.ROWS_PER_PAGE` (~l.2050-2055) for locality.
|
|
86
|
+
|
|
87
|
+
Proposed keys (interpolated, i18next `{{var}}` syntax — do not reserialize the whole file through
|
|
88
|
+
`JSON.parse`/`JSON.stringify`, this is a `.ts` module, not JSON; a plain text insert keeps every
|
|
89
|
+
other entry byte-identical):
|
|
90
|
+
```ts
|
|
91
|
+
"UserList.DISPLAYED_ROWS": {
|
|
92
|
+
"de": "{{from}}–{{to}} von {{count}}",
|
|
93
|
+
"fr": "{{from}}–{{to}} sur {{count}}",
|
|
94
|
+
"en": "{{from}}–{{to}} of {{count}}",
|
|
95
|
+
"sw": "{{from}}–{{to}} ya {{count}}"
|
|
96
|
+
},
|
|
97
|
+
"UserList.DISPLAYED_ROWS_MORE_THAN": {
|
|
98
|
+
"de": "{{from}}–{{to}} von mehr als {{to}}",
|
|
99
|
+
"fr": "{{from}}–{{to}} sur plus de {{to}}",
|
|
100
|
+
"en": "{{from}}–{{to}} of more than {{to}}",
|
|
101
|
+
"sw": "{{from}}–{{to}} ya zaidi ya {{to}}"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
The `sw` wording reuses "ya" for "of"/possessive, already used this way elsewhere in the same file
|
|
105
|
+
(e.g. l.57 `"ruhusa ya kutuma mialiko"`, l.114 `"barua pepe ya kuweka upya"`) and "zaidi ya" ("more
|
|
106
|
+
than") as a plain compositional phrase — a reasonable best-effort construction, NOT a
|
|
107
|
+
native-speaker-verified translation; say so plainly in your own report rather than presenting it as
|
|
108
|
+
settled, same as you would for any other claim you can't independently verify.
|
|
109
|
+
|
|
110
|
+
Call site becomes:
|
|
111
|
+
```jsx
|
|
112
|
+
labelDisplayedRows={({ from, to, count }) =>
|
|
113
|
+
count === -1
|
|
114
|
+
? t('UserList.DISPLAYED_ROWS_MORE_THAN', '{{from}}–{{to}} of more than {{to}}', { from, to })
|
|
115
|
+
: t('UserList.DISPLAYED_ROWS', '{{from}}–{{to}} of {{count}}', { from, to, count })
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Existing test harness to extend, not duplicate
|
|
120
|
+
|
|
121
|
+
`tests/userScreenTranslations.test.jsx` already renders `UserListComponent` through a REAL
|
|
122
|
+
`i18next` instance pinned to German (`withGermanI18n`, ~l.22-30, built from
|
|
123
|
+
`authTranslations.*.de`) and asserts rendered German strings plus `expectNoEnglishDefaults()`
|
|
124
|
+
(~l.34-39, checks `document.body.textContent` against an `ENGLISH_DEFAULTS` list, ~l.33-46). The
|
|
125
|
+
test `'renders UserListComponent in German, including pagination and action feedback'` (~l.97-125)
|
|
126
|
+
already fetches exactly 1 user, so the pagination currently renders English "1–1 of 1" — add that
|
|
127
|
+
literal string (or your chosen German text) to the existing `for (const german of [...])` loop
|
|
128
|
+
(~l.115) to assert the real rendered pagination text, and add the English range phrasing (e.g.
|
|
129
|
+
`'of 1'` or similar, whatever actually changes) to `ENGLISH_DEFAULTS` so `expectNoEnglishDefaults()`
|
|
130
|
+
(already called at line 124) starts actually catching a regression here — today it does NOT, since
|
|
131
|
+
no "of N" string is in that list, which is why this bug shipped unnoticed.
|
|
132
|
+
|
|
133
|
+
Required test 1 (German vs English) is naturally satisfied by extending this file: German via the
|
|
134
|
+
existing `withGermanI18n` harness, English via a parallel small i18next instance (or reuse the
|
|
135
|
+
`en` resources the same way `germanResources` is built from `.de`) rendering the same component and
|
|
136
|
+
asserting the English range text.
|
|
137
|
+
|
|
138
|
+
Required test 2 (key-parity across all four locales) — check whether a generic "every key has
|
|
139
|
+
every locale" structural test already exists in this repo (grep `authTranslations` test files for
|
|
140
|
+
something like `Object.values(...).every(...)` or a locale-set check) before writing a new one;
|
|
141
|
+
extend it if so, add a small standalone one for just these two new keys if not.
|
|
45
142
|
|
|
46
143
|
## Target repo working directory (absolute)
|
|
47
144
|
|
|
@@ -77,4 +174,38 @@ placeholder.)*
|
|
|
77
174
|
> Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
|
|
78
175
|
> reviewers, no verification run, no register edit, no commit.**
|
|
79
176
|
|
|
80
|
-
|
|
177
|
+
**Execution directive:** dispatch via `codex exec --skip-git-repo-check
|
|
178
|
+
--dangerously-bypass-approvals-and-sandbox -m gpt-5.6-luna - < work-orders/UCM-I18N-5.md` (today,
|
|
179
|
+
2026-10-02, already recorded `available` in `webapps/.claude/codex-status.md`), cwd =
|
|
180
|
+
`C:\Users\biglmi\Documents\webapps\ui-core-micha`. Monitor for `RESULT: DONE|BLOCKED`.
|
|
181
|
+
|
|
182
|
+
**Review routing:** Tier 3 (shared-core) — full `reviewer` lens set (`envelope`, `regression`,
|
|
183
|
+
`duplication`, `tests`, codex/gpt-5.6-luna, one concurrent batch, diff inline) PLUS `ui_reviewer`
|
|
184
|
+
(frontend-adjacent: a rendered component's i18n text). `duplication` should specifically check
|
|
185
|
+
whether any OTHER app already patched around this same MUI default locally (a local workaround that
|
|
186
|
+
this shared-core fix should make redundant, worth naming even if out of this WO's scope to remove).
|
|
187
|
+
|
|
188
|
+
**Tests:** the Orchestrator re-runs `tests/userScreenTranslations.test.jsx` (and any new/renamed
|
|
189
|
+
key-parity test file) as the gate — not ucm's full suite (Tier 3 but a small, local-only change;
|
|
190
|
+
no cross-app ripple to justify a full run per `AGENTS.md` → Test scope).
|
|
191
|
+
|
|
192
|
+
**Release:** patch bump (3.11.0 → 3.11.1) — this extends `UCM-I18N-4`'s existing i18n-gap category
|
|
193
|
+
in the same component, not a new capability area, per the project convention (version bump reflects
|
|
194
|
+
scope, not mere newness). Update `package.json` + `CHANGELOG.md`, then the repo's normal publish
|
|
195
|
+
path (check `.github/workflows/` or `package.json` scripts for how a previous release like
|
|
196
|
+
`UCM-THEME-14`/3.11.0 or `UCM-AUTH-11`/3.9.1 actually published, don't assume `npm publish` by
|
|
197
|
+
hand — follow the established mechanism).
|
|
198
|
+
|
|
199
|
+
**jg-ferien pin bump:** after the release is live, bump `ui-core-micha`'s pin in
|
|
200
|
+
`jg-ferien/frontend/package.json`, install, run jg-ferien's own affected tests (anything touching
|
|
201
|
+
`UserListComponent` consumption, if jg-ferien renders it at all — check first; if jg-ferien doesn't
|
|
202
|
+
use `UserListComponent`, the pin bump itself plus jg-ferien's existing suite staying green is the
|
|
203
|
+
evidence), commit + push to jg-ferien's `develop`.
|
|
204
|
+
|
|
205
|
+
**Register:** `done` only once BOTH halves land — the release AND the jg-ferien pin bump (the WO's
|
|
206
|
+
own Tier line: "Done means released AND jg-ferien's pin bumped"). Row here (ui-core-micha's
|
|
207
|
+
`WORK_ORDERS.md`) AND a row in jg-ferien's own `WORK_ORDERS.md` noting the pin bump, per the
|
|
208
|
+
Envelope's own closing instruction.
|
|
209
|
+
|
|
210
|
+
**Commit:** ui-core-micha lands on `main` (no `develop` branch here, confirmed in `AGENTS.md`'s
|
|
211
|
+
branch table). jg-ferien's pin bump lands on `develop` as usual.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# UCM-THEME-16: Dark-safe info box in `QrSignupManager`
|
|
2
|
+
|
|
3
|
+
# A. Envelope, authored by the Expertenchat
|
|
4
|
+
|
|
5
|
+
## Goal & expected outcome
|
|
6
|
+
|
|
7
|
+
The "Signup Access / valid until" box under a generated signup QR code stays readable in dark mode. Today it
|
|
8
|
+
paints a fixed light ground, `grey.50` (`#FAFAFA`), under the theme's secondary text; in dark that is
|
|
9
|
+
`#AAB5BF` on `#FAFAFA`, **2.00:1**. After this order the box takes `background.subtle`, which resolves per
|
|
10
|
+
mode: light `#F4F5F6` (5.37:1 for its text), dark `#20262C` (7.32:1).
|
|
11
|
+
|
|
12
|
+
## Decided (operator, 2026-10-02)
|
|
13
|
+
|
|
14
|
+
- **`src/components/QrSignupManager.jsx:376`**: `bgcolor: 'grey.50'` -> `bgcolor: 'background.subtle'`.
|
|
15
|
+
**The light change is accepted**: `#FAFAFA` -> `#F4F5F6`, a 1.05:1 step, barely visible.
|
|
16
|
+
- **`src/components/QrSignupManager.jsx:361`** (`bgcolor: '#ffffff'` behind the QR code itself) **stays white
|
|
17
|
+
on purpose**: a QR code needs a light quiet zone to scan. Recorded in `docs/DARK-TOKENS.md`.
|
|
18
|
+
- **`src/messaging/ReadTicks.jsx:14`** (`DM_SENT_COLOR = 'grey.500'`) **stays unchanged.** It already clears
|
|
19
|
+
contrast in dark (6.07:1 on the dark paper). The candidate `text.disabled` (`#6A7178`) is the value the file's
|
|
20
|
+
own comment records as rejected, because it read too close to the "read" state. In light the tick sits at
|
|
21
|
+
2.68:1 on white. That is a pre-existing condition, softened by the icon shape (Done vs DoneAll), and not
|
|
22
|
+
part of this order.
|
|
23
|
+
|
|
24
|
+
These decisions replace the register row's original question ("accept the small light change, or add a
|
|
25
|
+
token"), raised by the `UCM-THEME-14` review.
|
|
26
|
+
|
|
27
|
+
## Scope + non-goals
|
|
28
|
+
|
|
29
|
+
In scope: the one property at `QrSignupManager.jsx:376`; `CHANGELOG.md`; a **patch** release.
|
|
30
|
+
|
|
31
|
+
Non-goals: no change to `ReadTicks.jsx`, to the QR code's white ground, or to any token value.
|
|
32
|
+
|
|
33
|
+
## Tier · precondition / gate
|
|
34
|
+
|
|
35
|
+
- **Tier 3 · tests: one rendered-style assertion per mode for the info box; the component's existing tests.**
|
|
36
|
+
A change inside shared-core, even at one line (`AGENTS.md` -> Tiering).
|
|
37
|
+
- Precondition: `UCM-THEME-14` landed (3.11.0, confirmed 2026-10-02).
|
|
38
|
+
|
|
39
|
+
## Risks
|
|
40
|
+
|
|
41
|
+
- The light change is the only light-output change since `UCM-THEME-14`'s regression fixture. If that fixture
|
|
42
|
+
covers this component, it needs this one entry updated, named, and nothing else.
|
|
43
|
+
|
|
44
|
+
## Required tests to WRITE (you write them and run YOUR OWN new ones; the Orchestrator's run is the gate)
|
|
45
|
+
|
|
46
|
+
1. In a light theme from `createAppTheme`, the info box's background resolves to `#F4F5F6`; in a dark theme to
|
|
47
|
+
`#20262C`. Assert against the theme's own `background.subtle`, not a copied hex.
|
|
48
|
+
|
|
49
|
+
Run, do not write: the existing `QrSignupManager` tests and the light regression fixture.
|
|
50
|
+
|
|
51
|
+
## Parity guardrail
|
|
52
|
+
|
|
53
|
+
No behaviour change. One background, both modes.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# B. Implementation map, filled by the Orchestrator and ADDRESSED TO THE IMPLEMENTER
|
|
58
|
+
|
|
59
|
+
*Placeholder. The Orchestrator fills the context package, the absolute working directory, the
|
|
60
|
+
progress contract and the preamble block on `git pull`, per `AGENTS.md` -> "Work Order". A one-line change
|
|
61
|
+
may be implemented by the Orchestrator directly; authorship then sits with it and the independent review
|
|
62
|
+
carries independence. Do not dispatch while this placeholder stands.*
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
# C. Orchestrator only, NOT ADDRESSED TO THE IMPLEMENTER
|
|
67
|
+
|
|
68
|
+
> **If you are the implementer reading this work order as your own specification: STOP at this line.**
|
|
69
|
+
> Everything below describes what the Orchestrator does AFTER you finish. You do none of it: no
|
|
70
|
+
> reviewers, no verification run, no register edit, no `git add`/`commit`/`push`.
|
|
71
|
+
|
|
72
|
+
- Check `.claude/codex-status.md` first (newest-first). Before `git push`, run `git log origin/main..HEAD` and
|
|
73
|
+
push only if every listed commit is this order's.
|
|
74
|
+
- Review: Tier 3, independent `reviewer` (all configured lenses) and `ui_reviewer`, one batch, before the
|
|
75
|
+
commit.
|
|
76
|
+
- Release: verify the publish through the job log (`npm view` lags for provenance publishes). Row -> `done`
|
|
77
|
+
with the review Notiz and the version.
|
|
78
|
+
|
|
79
|
+
Mini-handover: `Orchestrator: implement work-orders/UCM-THEME-16.md in ui-core-micha (main). git pull first,
|
|
80
|
+
read the WO, then follow orchestrate-codex.`
|