@micha.bigler/ui-core-micha 3.5.0 → 3.5.1

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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Only notable, user-facing changes. Not every version — see `WORK_ORDERS.md` for the full history.
4
4
 
5
+ ## 3.5.1 — AUTH-7
6
+
7
+ `loginWithPassword`'s "already authenticated" (409) retry — the re-fetch of the current user when
8
+ allauth reports a still-valid session — no longer lets a second failure (e.g. the account's own
9
+ request quota being exhausted elsewhere) escape as a raw, unnormalised error. It now goes through
10
+ the same `normaliseApiError` path every other login failure already uses, so the caller (any app's
11
+ `LoginPage`) gets a `.code` it can translate instead of a bare exception. The success path (409 +
12
+ successful retry) is byte-identical; no prop/behavior contract changed for any existing caller.
13
+
5
14
  ## 3.5.0 — MSG-19
6
15
 
7
16
  `DirectMessageLauncher`'s recipient picker is now a searchable MUI `Autocomplete` instead of a
package/WORK_ORDERS.md CHANGED
@@ -27,6 +27,8 @@ prefixed ID; never reuse a bare flat number across workstreams.
27
27
 
28
28
  | ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
29
29
  |---|---|---|---|---|---|---|
30
+ | 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). |
31
+ | AUTH-8 | `extractErrorInfo`s `GENERIC`-Code ueberschreibt jeden `defaultCode` | Bei jedem DRF-Fehler mit `{"detail": "..."}`-Form (u.a. Django `Throttled`/`Http404`/`PermissionDenied` — genau die Form, die den urspruenglichen 429 aus `kira` ausloeste) liefert `extractErrorInfo` (`src/utils/auth-errors.js`) `code: 'GENERIC'`; `normaliseApiError`s `info.code \|\| defaultCode` waehlt dann IMMER `'GENERIC'`, nie den vom Aufrufer uebergebenen `defaultCode` (z. B. `'Auth.LOGIN_FAILED'`). `'GENERIC'` ist an keiner Stelle als i18n-Key registriert — die UI zeigt vermutlich den rohen String statt einer Uebersetzung. **Betrifft JEDE bestehende `normaliseApiError(...)`-Call-Site in diesem Repo**, nicht nur `loginWithPassword`. | 2026-09-05 | planned | | Gefunden von `ui_reviewer` waehrend `AUTH-7`s Review, empirisch bestaetigt (`node --input-type=module -e "..."` gegen die echte Funktion). **Kein WO-File, keine Implementierung — Operator-Entscheidung noetig**: Tier vermutlich 3 (Shared-Core, potenziell viele Call-Sites betroffen, Fix-Optionen (z. B. `defaultCode` vor `'GENERIC'` bevorzugen) muessen gegen alle bestehenden Aufrufer geprueft werden, die sich moeglicherweise auf `'GENERIC'` verlassen). |
30
32
  | MSG-19 | `DirectMessageLauncher` recipient picker: searchable Autocomplete replaces the flat List | Operator request: the new-DM recipient picker had no search, the same friction the fleet's own "Einfache Bedienung schlägt Vollständigkeit" rule already names ("a picker without search stops working long before the list feels long"). The `List`/`ListItemButton` picker is replaced with an MUI `Autocomplete` bound to the same `selectedId`/`selectedCandidate` state; the component's public prop contract (`candidates`, `scope`, `onOpen`) and its `start()`/scope-resolution logic are unchanged — only the picker's own rendering and selection mechanism changed. | 2026-09-05 | done | 7d99d82 | No WO file — operator-requested, fixed directly. Tier 3 (shared-core, `ui-core-micha`). Only current consumer of this component: jg-ferien (confirmed via a fleet-wide grep for `DirectMessageLauncher`), and jg-ferien's own tests fully mock it, so this change has zero blast radius there until jg-ferien separately bumps its `@micha.bigler/ui-core-micha` pin. Implemented directly in Claude (no Codex dispatch — pure component-level UI change). **review: claude/sonnet · 0 raised** (verified null-safety of `isOptionEqualToValue`/`getOptionLabel` against MUI's own `useAutocomplete` source, confirmed the rewritten test file preserves every one of the original 7 cases plus one new filtering case, confirmed i18n completeness) + **ui_reviewer: claude/sonnet · 4 raised · 4 accepted · worst accepted: defect** — (U1: the new "typed query matches nothing" state had no `noOptionsText` and would have shown MUI's hardcoded, untranslated English default — added `MessagingDirect.NO_MATCHES` in all 4 languages plus a test; U2: the Autocomplete was missing `fullWidth`, breaking from this repo's own one other Autocomplete-in-Dialog convention (`SupportRecoveryRequestsTab.jsx`) and regressing from the old List's full-width block layout — added; U3: reused `MessagingDirect.CANDIDATES`'s old plural-noun wording ("Recipients") as a visible single-select field label read awkwardly — reworded to an instruction-style label ("Select recipient") matching the sibling component's own convention, in all 4 languages; U4 (inferred, not rendered/verified): `autoFocus` on the TextField combined with the dialog's `fullScreen` mobile breakpoint could open the virtual keyboard unprompted — removed `autoFocus` entirely, since MUI's own `Dialog` already focuses the first focusable descendant on open by design, making it redundant). Two additional self-found nits fixed post-review: a defensive null-guard added to `candidateLabel` (matching the sibling `getUserOptionLabel` convention, even though confirmed unreachable under MUI's current calling contract) and a one-line update to `docs/messaging-deviations.md` item 18 (stale reference to "displays... candidate list" not mentioning the new search). Tests: `messagingDirectMessageLauncher.test.jsx` rewritten (9/9 green, up from 7 — the rewrite required discovering and documenting a real jsdom/RTL gotcha: MUI's `Autocomplete` only reacts to a typed value once the input is genuinely `.focus()`-ed, not merely `mouseDown`-ed) + `i18nAggregate.test.js` (4/4) both green; `tsc -p tsconfig.build.json` clean. Version bumped 3.4.0 → 3.5.0 (minor — additive UI capability, no prop-contract change) via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately, in its own repo, once this lands on npm. |
31
33
  | AUTH-6 | `QrSignupManager` additive `registrationContext`/`defaultMaxRedemptions` props | jg-ferien's `JG-MEM-1` (department QR-join) needs a per-department signup context and a high, poster-scale redemption default — `QrSignupManager` only exposed `enabled`/`expiryDays`. Two new optional props added, both defaulting to today's exact behaviour: `registrationContext` (merged into the generated token's `registration_context`) and `defaultMaxRedemptions` (overrides the low internal default). | 2026-08-28 | done | 1749c09 | No WO file — operator-requested, fixed directly, same convention as AUTH-3. Tier 3 (shared-core). Implemented by `codex exec` (jg-ferien's JG-MEM-1 dispatch touched this repo additively per that WO's own allowance), left uncommitted for the Orchestrator's own review before landing here. **review: claude/sonnet · 2 raised · 2 accepted · worst accepted: defect** — (R1: the new context-change effect also calls `setMaxRedemptions`, and with `maxRedemptions` in that same effect's dependency array, a context change after the admin had typed a custom redemption count fired a second, redundant `createSignupQr` call — fixed by reading the current value through a ref instead of a reactive dependency, so `setMaxRedemptions` no longer re-triggers the effect that just set it; R2: the one new test only covered the happy path on a fresh mount, with no test proving the existing `AccountPage` call (`enabled`/`expiryDays` only) stays byte-for-byte unaffected, and no test that would have caught R1 — two regression tests added: a bare old-prop-shape call asserting the exact legacy payload + single call, and a context-change-after-custom-redemptions call asserting exactly one `createSignupQr` invocation). Confirmed backward compatible against the one existing consumer, `AccountPage.jsx:369-372` (unaffected — it never passes the new props). Tests: `QrSignupManager.registrationContext.test.jsx` (3/3, incl. the two new regression cases) + `AccountPage.test.jsx` (4/4) + `SignUpPage.test.jsx` (10/10) all green — 17/17 total across the three files touching this component. No `ui_reviewer`/`sec_reviewer` (pure prop/effect-timing change, no visual surface, no auth-enforcement logic — the actual token validation stays entirely server-side in `django-core-micha`, already reviewed under `JG-MEM-1`). **Rebased onto a concurrent session's `UCM-CHART-17`/`-18` pushes (3.3.0/3.3.1 already published) — republished as 3.4.0 instead of the originally-committed 3.3.0** (minor — new capability, not a fix), via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately once this lands on npm. |
32
34
  | UCM-CHART-18 | The PNG export shows what the chart says, not what the user can click | Two defects, one file, a few lines apart -- both raised by `HRAM-CHT-7`'s independent `ui_reviewer` (2026-08-25) and confirmed, partly by reading and partly live. **(1) Interactive controls are in the image.** Since 3.3.0 the PNG rasterises `ChartFrame`'s whole `chartRef` box, and `chartRef` wraps `children` only -- not `toolbar`, `controls` or `meta` -- so anything a panel renders as children is now in the file: hram's `StructuralReachabilityPanel:468` footer `IconButton` (always visible, so **every** PNG of that panel carries a button), `AllocationPerformancePanel:536,548` and `OptimizationResultsPanel:1120,1141`. **(2) `transform` is not inlined** -- neither property list (`:32-36`, `:44-52`) has it, so `OptimizationResultsPanel:1107`'s rotated legend swatch exports as a **square instead of a diamond**. **Fixed here rather than in the app for two reasons:** it is a property of the element kind, not of each panel -- an exported image should show what the chart communicates, not what can be clicked -- and the alternative (moving controls into `ChartFrame`'s `controls`/`toolbar` slots) would **undo a deliberate design decision**, since `HRAM-RES-30` put that info affordance in the footer beside the caveat line. After this, hram needs **only a pin bump**. Full order [`work-orders/UCM-CHART-18.md`](work-orders/UCM-CHART-18.md) | 2026-08-25 | done | `5f34a2b` | **Tier 3** -- shared-core. `ui_reviewer` (what belongs in an exported image, and the MUI-legend counter-check) + `reviewer` (the ordering trap, the `transform` double-apply question); no `sec_reviewer`. **THE single most likely way to get this wrong:** `inlineComputedStyles` walks source and clone **in lockstep by index** (`:70-77`), so removing controls from the clone **before** it shifts every subsequent index and elements silently receive their neighbours' styles -- an export that looks plausible and is wrong everywhere after the first removal. Removal happens **after** the style walk, and the code comment must say so. **Counter-check that is not optional:** what disappears must be operation, not content -- a consumer whose legend items are `<button>`s (MUI's own legend can be interactive) would lose its legend, and the affected hram panels all pass `hideLegend` so they are **not** the test case; verify against a chart that keeps MUI's own legend, and if it vanishes that changes the approach. Also: `transform` may **double-apply** on SVG nodes that carry it as a presentation attribute the clone already keeps -- check `xLabels="angled"` before assuming it is inert. **Release: patch -> 3.3.1** -- this repairs behaviour 3.3.0 itself introduced rather than adding capability, the same reasoning `UCM-CHART-16` went out on, while `-15`/`-17` were minors because they changed what a consumer gets on purpose. Gate is **opening the files**, as for `UCM-CHART-17`: both defects were invisible to every test and visible immediately in an exported image. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly, no Codex dispatch. The lockstep-index ordering was respected from the start (`inlineComputedStyles` before `stripInteractiveControls`), `transform`/`transform-origin` added to the shared property list. **Verification (real Chrome, via the dev harness's module graph, no probe entry ever committed):** all four WO cases confirmed with hard evidence, not visual impression -- (1) a control in `children` (`IconButton` beside static text): captured the PNG path's actual intermediate SVG, confirmed no `<button` tag/text while the sibling and chart survive. (2) MUI's own legend (non-interactive, two series): fully intact, both labels present. (3) a rotated swatch: pixel-sampled the real rasterised PNG on a real `<canvas>` -- bounding-box corner reads background white, centre reads the swatch's own colour, i.e. a genuine diamond. (4) `xLabels="angled"`: the exported tick carries both the original `transform` XML attribute (kept by `cloneNode`) and the newly-inlined computed matrix; the matrix values mathematically match a SINGLE -45° rotation (`cos(-45°)=0.707107`), not a doubled one -- confirmed the inline `style` declaration wins over the presentation attribute rather than composing with it. 89 affected chart tests green (14 in `exportChart.test.js`), `tsc` clean. **Review: codex/gpt-5.6-luna (`reviewer` + `ui_reviewer`, concurrent) · 5 raised · 3 accepted · worst accepted: high (interactive-legend content loss)** (id not divisible by 5, no rotation configured). **Both reviewers independently converged on the SAME defect**, exactly the risk the WO's own Envelope named and required a live counter-check for: the initial fix REMOVED (not unwrapped) MUI's own interactive legend items (`onItemClick`/`toggleVisibilityOnClick` renders each item as `<button role="button" class="...MuiChartsLegend-series...">`), deleting the swatch+label along with the click handler -- the WO's own words, "if it vanishes, that is a finding that changes the approach, not a detail to paper over." FIXED: `MuiChartsLegend-series` matches are now UNWRAPPED (replaced with a plain `<span>` carrying the same class/style/children) instead of removed; re-verified live against an ACTUAL interactive legend (`slotProps={{legend:{toggleVisibilityOnClick:true}}}`, confirmed the live items render as real `<BUTTON>` tags) -- export contains zero `<button` tags, `MuiChartsLegend-series` and both series labels survive intact. `reviewer` additionally found: the interactive-control selector missed `a[href]`/`[role="link"]` (accepted, added -- also corrects the CHANGELOG's own "drill-down links" claim, which wasn't true until this fix); and the original ordering test's button was the LAST descendant, so a wrong call order wouldn't have shifted any node the test actually asserted on -- content-presence checks can't observe a style misattribution, only a wrong STYLE VALUE can (accepted, rewrote with a distinctively-styled trailing sibling, confirmed by temporarily reverting the call order that the new assertion genuinely fails). Two findings rejected as decoration/informational only (transform double-apply already reasoned through correctly by both reviewers; a jsdom-literal vs. browser-matrix distinction in the unit test, already covered by the separate real-browser verification track). |
@@ -107,8 +107,13 @@ export async function loginWithPassword(email, password) {
107
107
  }
108
108
  // 409 = Already logged in
109
109
  if (status === 409) {
110
- const user = await fetchCurrentUser();
111
- return { user, needsMfa: false };
110
+ try {
111
+ const user = await fetchCurrentUser();
112
+ return { user, needsMfa: false };
113
+ }
114
+ catch (retryError) {
115
+ throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
116
+ }
112
117
  }
113
118
  throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
114
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "sideEffects": [
@@ -114,8 +114,12 @@ export async function loginWithPassword(email, password) {
114
114
 
115
115
  // 409 = Already logged in
116
116
  if (status === 409) {
117
- const user = await fetchCurrentUser();
118
- return { user, needsMfa: false };
117
+ try {
118
+ const user = await fetchCurrentUser();
119
+ return { user, needsMfa: false };
120
+ } catch (retryError) {
121
+ throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
122
+ }
119
123
  }
120
124
 
121
125
  throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
@@ -0,0 +1,42 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const client = vi.hoisted(() => ({
4
+ post: vi.fn(),
5
+ get: vi.fn(),
6
+ }));
7
+
8
+ vi.mock('../src/auth/apiClient', () => ({ default: client }));
9
+
10
+ import { loginWithPassword } from '../src/auth/authApi';
11
+
12
+ describe('loginWithPassword', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ it('returns the current user when login reports an existing session', async () => {
18
+ const user = { id: 1, email: 'ada@example.com' };
19
+ client.post.mockRejectedValue({ response: { status: 409 } });
20
+ client.get.mockResolvedValue({ data: user });
21
+
22
+ await expect(loginWithPassword('ada@example.com', 'password')).resolves.toEqual({
23
+ user,
24
+ needsMfa: false,
25
+ });
26
+ });
27
+
28
+ it('normalizes a failure while retrying the current user after a 409', async () => {
29
+ const retryError = {
30
+ response: { status: 429, data: { detail: 'Too many requests' } },
31
+ };
32
+ client.post.mockRejectedValue({ response: { status: 409 } });
33
+ client.get.mockRejectedValue(retryError);
34
+
35
+ await expect(loginWithPassword('ada@example.com', 'password')).rejects.toSatisfy((error) => {
36
+ expect(error).toBeInstanceOf(Error);
37
+ expect(error.code).toBeDefined();
38
+ expect(error).not.toBe(retryError);
39
+ return true;
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,181 @@
1
+ # AUTH-7 — `loginWithPassword`'s "already authenticated" retry must not crash raw on a second failure
2
+
3
+ ---
4
+
5
+ # A. Envelope — authored by the Expertenchat
6
+
7
+ ## Goal & expected outcome
8
+ - Ziel: allauth-headless antwortet auf `POST /auth/login` mit `409` ("Conflict"), wenn bereits
9
+ eine gültige Session existiert (die Session ist real gültig — kein Fehlerfall an sich).
10
+ `loginWithPassword` (`src/auth/authApi.jsx:97-123`) behandelt das korrekt, indem es dann
11
+ `fetchCurrentUser()` erneut aufruft, um den bereits eingeloggten User zurückzugeben. Schlägt
12
+ DIESER Retry-Call aber ebenfalls fehl (beobachtet 2026-09-05: ein anderer, unabhängig
13
+ überlasteter Endpunkt hatte den globalen `user`-Throttle-Bucket geleert, sodass auch
14
+ `/api/users/current/` mit 429 antwortete), wird der Fehler ungefangen durchgereicht — kein
15
+ `.code`, keine Übersetzung, ein rohes Error-Objekt statt eines normalen Login-Fehlers.
16
+ - Expected outcome: schlägt der 409-Retry fehl, bekommt der Aufrufer (`LoginPage.jsx:219-220`
17
+ liest `err.code`) einen normalisierten Fehler mit `.code` — dieselbe Fehlerform wie jeder andere
18
+ Login-Fehlerpfad in dieser Funktion, kein Sonderfall.
19
+
20
+ ## Scope + non-goals
21
+ - In scope: ausschliesslich der 409-Zweig in `loginWithPassword`
22
+ (`src/auth/authApi.jsx:116-119`) — den bereits vorhandenen `fetchCurrentUser()`-Retry-Call in
23
+ ein eigenes try/catch nehmen und einen Fehlschlag über `normaliseApiError` weiterwerfen, exakt
24
+ wie jeder andere Fehlerpfad in dieser Funktion (Zeile 121) es bereits tut.
25
+ - Explizite Non-Goals / nicht anfassen: der **Erfolgsfall** dieses Zweigs (409 + erfolgreicher
26
+ Retry → `{ user, needsMfa: false }`) bleibt bytegleich. `AuthContext.jsx`s Bootstrap-Catch-Block
27
+ (Zeile 107-109) wird NICHT geändert — geprüft: `user` ist beim ersten Mount ohnehin bereits
28
+ `null`, eine Fallunterscheidung nach Statuscode hätte dort unter keinem real erreichbaren
29
+ Codepfad einen beobachtbaren Effekt (der Effect läuft genau einmal pro Seitenaufruf), wäre also
30
+ Änderung ohne Wirkung in einer Shared-Core-Datei — bewusst ausgelassen. Keine neue Kontext-Prop,
31
+ keine Änderung an einer konsumierenden App (`kira` o.a.) — der Fix ist rein additiv/intern zu
32
+ `authApi.jsx`. `package.json`s Versionsfeld wird als Teil dieser WO angepasst (PATCH-Bump, siehe
33
+ Teil C) — das ist Prozess/Publish-Mechanik dieses Repos, kein Scope-Zusatz am eigentlichen Fix.
34
+
35
+ ## Tier · precondition / gate
36
+ - Tier: 3 — Änderung INNERHALB von Shared-Core (`ui-core-micha`), unabhängig vom Umfang (AGENTS.md
37
+ → Tiering-Tabelle). Kein Precondition/Gate; die Änderung ist rein additiv (ein neuer catch um
38
+ einen bestehenden Call), kein Breaking Change, kein Versions-Bump nötig, keine konsumierende App
39
+ muss etwas tun.
40
+
41
+ ## Risks
42
+ - Der Erfolgspfad (409 + Retry gelingt) darf sich nicht ändern — sonst bricht der bestehende
43
+ "bereits eingeloggt"-Login-Flow für ALLE Apps, die `ui-core-micha` konsumieren.
44
+ - Der neue catch darf den `normaliseApiError`-Fehler nicht verschlucken oder verfälschen — die
45
+ aufrufende `LoginPage.jsx` liest `err.code` für die i18n-Anzeige.
46
+
47
+ ## Required tests to WRITE
48
+ - Neue Datei `tests/authApiLogin.test.js` (Muster: `tests/authApiRegistration.test.js` — `apiClient`
49
+ gemockt via `vi.mock`, `client.post`/`client.get` direkt gestubbt, kein React/Testing-Library
50
+ nötig, `loginWithPassword` direkt importiert und aufgerufen):
51
+ 1. `post` rejects mit `{ response: { status: 409 } }`, `get` (der `fetchCurrentUser`-Retry)
52
+ resolved mit `{ data: { id: 1, ... } }` → `loginWithPassword` liefert
53
+ `{ user: {...}, needsMfa: false }` (dokumentiert den unveränderten Erfolgspfad).
54
+ 2. `post` rejects mit `{ response: { status: 409 } }`, `get` (der Retry) rejects zusätzlich mit
55
+ `{ response: { status: 429, data: { detail: '...' } } }` → `loginWithPassword` wirft einen
56
+ Error mit einem gesetzten `.code` (nicht `undefined`, nicht das rohe Axios-Error-Objekt).
57
+
58
+ ---
59
+
60
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
61
+
62
+ ## Context package
63
+
64
+ **Named file to change:** `src/auth/authApi.jsx:97-123` (`loginWithPassword`), konkret nur
65
+ Zeilen 115-119:
66
+
67
+ ```js
68
+ // 409 = Already logged in
69
+ if (status === 409) {
70
+ const user = await fetchCurrentUser();
71
+ return { user, needsMfa: false };
72
+ }
73
+
74
+ throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
75
+ ```
76
+
77
+ Ändere zu (Retry in eigenes try/catch, Fehlschlag desselben `normaliseApiError`-Wegs wie jeder
78
+ andere Fehlerpfad dieser Funktion):
79
+
80
+ ```js
81
+ // 409 = Already logged in
82
+ if (status === 409) {
83
+ try {
84
+ const user = await fetchCurrentUser();
85
+ return { user, needsMfa: false };
86
+ } catch (retryError) {
87
+ throw normaliseApiError(retryError, 'Auth.LOGIN_FAILED');
88
+ }
89
+ }
90
+
91
+ throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
92
+ ```
93
+
94
+ `normaliseApiError` ist bereits oben in der Datei importiert (Zeile 3, aus `../utils/auth-errors`)
95
+ — keine neue Abhängigkeit.
96
+
97
+ **Invariants / Pitfalls:**
98
+ - `fetchCurrentUser()` (Zeile 16-24, gleiche Datei) wirft bei jedem Fehler das **rohe** Axios-Error
99
+ weiter (kein eigener catch dort) — das ist bereits der Zustand, den dieser Fix abfängt; nicht
100
+ `fetchCurrentUser()` selbst ändern.
101
+ - Der äussere `catch (error)`-Block (Zeile 103) bleibt unverändert; nur der 409-Zweig innerhalb
102
+ bekommt einen zusätzlichen inneren try/catch. Die MFA-Prüfung (Zeile 108-112) und der finale
103
+ `throw normaliseApiError(error, ...)` (Zeile 121, für den ursprünglichen `error`, NICHT den
104
+ `retryError`) bleiben unverändert.
105
+ - `normaliseApiError`s zweites Argument ist ein Default-Code, kein spezifischer 429-Code — es gibt
106
+ keinen eigenen i18n-Key für "Login gerade nicht möglich, Konto ist gedrosselt"; Wiederverwendung
107
+ von `'Auth.LOGIN_FAILED'` (bereits vorhandener Key) ist beabsichtigt, kein neuer i18n-Key nötig
108
+ in irgendeiner konsumierenden App.
109
+
110
+ Directive: Arbeite ausschliesslich aus diesem Kontext-Paket; öffne nur die genannte Datei plus die
111
+ genannte Testdatei-Vorlage zur Verifikation. Kein breites Erkunden im restlichen `src/auth/`.
112
+
113
+ ## Target repo working directory (absolute)
114
+
115
+ `C:\Users\Micha Bigler\Documents\webapps\ui-core-micha`
116
+
117
+ ## Preamble — REQUIRED, part of this file
118
+
119
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine;
120
+ > there is no separate plan file. Read the nearest `AGENTS.md`, the relevant
121
+ > `.codex/skills/<role>/SKILL.md`, and the app `MEMORY.md` ONLY for conventions. Stay in scope; do
122
+ > not touch auth/permissions/deps/schema/CI unless the spec says so; do not update `MEMORY.md`.
123
+ > **Do NOT edit `WORK_ORDERS.md`** — the register row and the review verdicts are the
124
+ > orchestrator's alone. **Your tools are for editing source and test files and for running the
125
+ > tests you wrote — nothing else.** Do NOT install dependencies, touch a lockfile, run a package
126
+ > manager, or tidy up stray files; if something in the repo state blocks you, stop and report it as
127
+ > `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT `git add`/`commit`/`push` — leave every
128
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the
129
+ > tests the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm
130
+ > they execute and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT
131
+ > any review). The orchestrator re-runs the authoritative set + does the independent review after
132
+ > you finish — those are the gate; your own run does not count as the gate.
133
+ >
134
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
135
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
136
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
137
+ > `RESULT: DONE|BLOCKED <reason>`.
138
+
139
+ ---
140
+
141
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
142
+
143
+ > **If you are the implementer reading this work order as your own specification: STOP at this
144
+ > line. Everything below describes what the Orchestrator does AFTER you finish. You do none of it
145
+ > — no reviewers, no verification run, no register edit, no commit.** You ARE the invocation
146
+ > described below; do NOT shell out to `codex exec`.
147
+
148
+ ## Execution directive
149
+
150
+ Implement through `codex exec` in the background, invoked directly via Bash with both mandatory
151
+ flags. Fallback to direct Claude implementation only on Codex quota/rate-limit/non-zero exit
152
+ (flips authorship — independent reviewer becomes mandatory, already mandatory at Tier 3).
153
+
154
+ ## Review routing
155
+
156
+ Tier 3, full set, all concurrent in one background batch: `reviewer` (all 4 lenses — this is
157
+ shared-core, `duplication` and `regression` lenses matter most here), `ui_reviewer` (diff touches
158
+ a `.jsx` file consumed by every app's login page — narrow context: this diff + the relevant
159
+ `AGENTS.md` auth-error-handling convention), `sec_reviewer` (auth/login flow). Orchestrator's own
160
+ targeted security pass runs alongside, additional not substitute.
161
+
162
+ ## Verification
163
+
164
+ Authoritative test run: `npx vitest run tests/authApiLogin.test.js tests/AuthContext.test.jsx
165
+ tests/authApiRegistration.test.js` (affected area — everything touching `authApi.jsx` and the
166
+ login flow it feeds). No prototype artifact in scope, no rendered side-by-side needed (no visual
167
+ change).
168
+
169
+ ## Register + commit
170
+
171
+ Bump `package.json`'s `version` `3.5.0` -> `3.5.1` (PATCH — bugfix, no interface/behavior change
172
+ for any existing caller) as part of this same commit, matching this repo's established
173
+ push-to-`main` + version-bump publish flow (see `MSG-19`'s register row for precedent). Commit on
174
+ `main` (this repo has no `develop`). `WORK_ORDERS.md` row for `AUTH-7`, named review verdict.
175
+ After push, verify the bumped version actually published (per
176
+ `reference_dcm_ucm_publish_topology` — publish-from-main has no staging gate, check the live
177
+ package version after push). **No consumer pin-bump is required for this to take effect for
178
+ kira** — this is a pure bugfix with no interface change, so record explicitly in the Notiz that no
179
+ app needs to bump its `@micha.bigler/ui-core-micha` pin for this fix to matter (kira's *symptom*
180
+ was caused by the throttle exhaustion fixed in `KIRA-TRACK-3`, not by this bug alone — this fix
181
+ only prevents a raw/uncaught error the NEXT time any app's login flow race is hit).