@micha.bigler/ui-core-micha 2.14.0 → 2.16.0

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.
Files changed (71) hide show
  1. package/README.md +7 -0
  2. package/WORK_ORDERS.md +8 -2
  3. package/dev/entries.jsx +147 -0
  4. package/dev/fixtures.js +26 -0
  5. package/dev/main.jsx +56 -0
  6. package/dev/mockTransport.jsx +128 -0
  7. package/dist/components/charts/BarChart.js +32 -0
  8. package/dist/components/charts/ChartFrame.js +50 -0
  9. package/dist/components/charts/LineChart.js +32 -0
  10. package/dist/components/charts/exportChart.js +67 -0
  11. package/dist/components/charts/formatters.js +23 -0
  12. package/dist/components/charts/palette.js +21 -0
  13. package/dist/i18n/chartsTranslations.js +38 -0
  14. package/dist/i18n/messagingTranslations.js +81 -0
  15. package/dist/index.js +25 -4
  16. package/dist/messaging/AttachmentList.js +43 -0
  17. package/dist/messaging/Composer.js +120 -0
  18. package/dist/messaging/ConversationLaunchers.js +39 -0
  19. package/dist/messaging/ConversationList.js +75 -0
  20. package/dist/messaging/MessageBubble.js +17 -0
  21. package/dist/messaging/MessagingProvider.js +317 -0
  22. package/dist/messaging/MessagingScopeConfig.js +28 -0
  23. package/dist/messaging/PollCard.js +53 -0
  24. package/dist/messaging/ReactionBar.js +44 -0
  25. package/dist/messaging/ReadTicks.js +25 -0
  26. package/dist/messaging/Thread.js +73 -0
  27. package/dist/messaging/api.js +51 -0
  28. package/dist/notifications/NotificationsProvider.js +2 -2
  29. package/dist/notifications/realtime.js +21 -1
  30. package/docs/messaging-deviations.md +36 -0
  31. package/index.html +12 -0
  32. package/package.json +6 -1
  33. package/src/components/charts/BarChart.jsx +44 -0
  34. package/src/components/charts/ChartFrame.jsx +112 -0
  35. package/src/components/charts/LineChart.jsx +44 -0
  36. package/src/components/charts/exportChart.js +71 -0
  37. package/src/components/charts/formatters.js +39 -0
  38. package/src/components/charts/palette.js +24 -0
  39. package/src/i18n/chartsTranslations.ts +38 -0
  40. package/src/i18n/messagingTranslations.ts +81 -0
  41. package/src/index.js +33 -4
  42. package/src/messaging/AttachmentList.jsx +38 -0
  43. package/src/messaging/Composer.jsx +110 -0
  44. package/src/messaging/ConversationLaunchers.jsx +51 -0
  45. package/src/messaging/ConversationList.jsx +78 -0
  46. package/src/messaging/MessageBubble.jsx +37 -0
  47. package/src/messaging/MessagingProvider.jsx +323 -0
  48. package/src/messaging/MessagingScopeConfig.jsx +24 -0
  49. package/src/messaging/PollCard.jsx +36 -0
  50. package/src/messaging/ReactionBar.jsx +46 -0
  51. package/src/messaging/ReadTicks.jsx +23 -0
  52. package/src/messaging/Thread.jsx +71 -0
  53. package/src/messaging/api.js +47 -0
  54. package/src/notifications/NotificationsProvider.jsx +2 -2
  55. package/src/notifications/realtime.jsx +21 -1
  56. package/tests/BarChart.test.jsx +55 -0
  57. package/tests/ChartFrame.test.jsx +122 -0
  58. package/tests/ConversationList.test.jsx +66 -0
  59. package/tests/LineChart.test.jsx +33 -0
  60. package/tests/MessagingProvider.test.jsx +50 -0
  61. package/tests/Thread.test.jsx +100 -0
  62. package/tests/chartsPalette.test.js +39 -0
  63. package/tests/messagingApi.test.js +32 -0
  64. package/tests/messagingComposer.test.jsx +111 -0
  65. package/tests/messagingExports.test.js +20 -0
  66. package/tests/messagingInteractions.test.jsx +112 -0
  67. package/tests/realtime.test.jsx +27 -0
  68. package/vite.config.mjs +6 -0
  69. package/work-orders/CHART-1.md +285 -0
  70. package/work-orders/DX-1.md +166 -0
  71. package/work-orders/MSG-3.md +375 -0
@@ -0,0 +1,166 @@
1
+ # DX-1 — minimal Vite dev harness for ucm components
2
+
3
+ Status: planned · Tier 1 (dev-only tooling; **adds devDependencies → approval-gated**) · Target repo: `ui-core-micha` (main)
4
+
5
+ ---
6
+
7
+ ## Part A — Envelope (Expertenchat, 2026-07-31)
8
+
9
+ ### Goal
10
+
11
+ Give `ui-core-micha` a way to **render** its components during development. Today `package.json`
12
+ exposes only `build` (tsc) and `test` (vitest): there is no Storybook, no demo app, no dev page. Every
13
+ shipped surface — auth, onboarding, notifications, charts — has only ever been verified in jsdom or
14
+ after a consuming app pinned it.
15
+
16
+ Immediate driver: MSG-3 is ~3700 LOC of new UI (jg's parity target: `Thread.jsx` alone is 2470 LOC)
17
+ that no app mounts until MSG-5, and the operator granted a redesign licence, which is only iterable
18
+ if the result can be looked at. But the gap is not messaging-specific and the harness is not
19
+ messaging-specific.
20
+
21
+ ### Expected outcome
22
+
23
+ - A `pnpm dev` (or equivalent) script starting a Vite dev server that mounts ucm components in a
24
+ browser, with the repo's MUI theme, `react-i18next` and router context wired the way a host app
25
+ wires them.
26
+ - A small set of harness entries covering the existing surfaces well enough to prove the harness is
27
+ real: at minimum one notifications surface and one charts surface. Adding an entry for a new
28
+ component must be a few lines, not a framework.
29
+ - **A single component must be mountable standalone**, not only a whole assembled surface. MSG-3
30
+ relies on this: it requires one harness entry per named component as its decomposition forcing
31
+ function, so the harness must not assume an entry equals a full page.
32
+ - A **mock transport layer** the entries drive: fixture data plus an injectable API/realtime adapter,
33
+ so a surface can be exercised without a backend. Where a component takes its data through a
34
+ provider, the harness supplies a mock provider rather than patching the component.
35
+ - Light/dark theme and viewport switching, since responsive and dark-mode correctness is exactly what
36
+ jsdom cannot show.
37
+ - Harness code lives outside the published package: not exported from `src/index.js`, excluded from
38
+ `tsconfig.build.json`, and adding **zero** runtime dependencies. `dist/` must be byte-identical in
39
+ content to a build from before this WO.
40
+ - `README` (or `CONTRIBUTING`) gains a short section on running the harness and adding an entry.
41
+
42
+ ### Non-goals / do-not-touch
43
+
44
+ Storybook (deliberately not chosen — a dev page is a fraction of the config surface and this repo has
45
+ no story-writing habit to build on; revisit only if the harness proves insufficient); visual
46
+ regression / screenshot testing; publishing the harness; any change to component source, exports, or
47
+ public API; any change to the existing vitest setup; CI wiring (the harness is a local dev tool, not
48
+ a gate).
49
+
50
+ ### Required tests to WRITE
51
+
52
+ This is dev tooling, so the meaningful assertions are about **not** affecting the product:
53
+
54
+ - The published barrel (`src/index.js`) is unchanged — the existing exports regression test
55
+ (`tests/notificationsExports.test.js` pattern) must still pass, and no harness module is reachable
56
+ from it.
57
+ - A build (`tsc -p tsconfig.build.json`) succeeds and emits no harness files into `dist/`.
58
+ - `package.json` `dependencies` and `peerDependencies` are unchanged; additions are confined to
59
+ `devDependencies`.
60
+
61
+ No component behaviour is under test here — the existing suite covers that and must stay green.
62
+
63
+ ### Risks
64
+
65
+ - **Dependency addition** is the real cost and the reason this is approval-gated: Vite plus a React
66
+ plugin. Keep it to the minimum; do not pull in a UI-catalog framework.
67
+ - **Harness fidelity.** A mock adapter can drift from the real REST/realtime contract and produce
68
+ confidence that does not transfer. Mitigation: fixtures are shaped from the actual dcm contract in
69
+ `django-core-micha/docs/design/messaging-platform.md` §REST/§Realtime, and the harness is a
70
+ development aid — it never replaces the tests or the independent review.
71
+ - Scope creep into "a component gallery". The deliverable is a dev page, not a product.
72
+
73
+ ### Preconditions
74
+
75
+ Operator approval for the devDependency additions — granted 2026-07-31 as part of the MSG-3 scoping
76
+ decision. This WO **runs before MSG-3**.
77
+
78
+ ### Execution directive
79
+
80
+ Implement through `codex exec` in the background — invoked **directly via Bash** (never the
81
+ `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
82
+ `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
83
+ fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
84
+
85
+ ### Release
86
+
87
+ **No version bump, no publish.** Dev-only tooling changes nothing in the package. The next ucm
88
+ publish rides whatever WO follows (MSG-3).
89
+
90
+ ### Mini-handover (pastable)
91
+
92
+ Orchestrator: implement `work-orders/DX-1.md` in `ui-core-micha` (main). `git pull` first, read the
93
+ WO, then follow `orchestrate-codex` (Codex-first, own independent review, commit on green, **no
94
+ publish**).
95
+
96
+ ---
97
+
98
+ ## Part B — Implementation map (Orchestrator)
99
+
100
+ ### Target repo / working directory
101
+
102
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root; package `@micha.bigler/ui-core-micha`,
103
+ current published version 2.15.0 — **unchanged by this WO**, no publish).
104
+
105
+ ### Context package
106
+
107
+ **Repo conventions to mirror:**
108
+ - `package.json` — current `scripts`: `build` (`tsc -p tsconfig.build.json`), `test` (`vitest run`).
109
+ No `dev` script exists yet; add one. `peerDependencies`/`dependencies` must not change;
110
+ `devDependencies` gains Vite + `@vitejs/plugin-react` only (or whatever minimal set a Vite+React+JSX
111
+ dev server needs) — no UI-catalog framework, no Storybook.
112
+ - `tsconfig.build.json` — `"include": ["src"]`, `rootDir: "./src"`, `outDir: "dist"`. The harness must
113
+ live **outside** `src/` (e.g. a top-level `dev/` directory) so it is never included by this config
114
+ and never emitted into `dist/`. Confirm after writing: `dist/` from a build with the harness present
115
+ must be byte-identical to a build from before this WO (no new/changed files under `dist/`).
116
+ - `src/index.js` — the published barrel. Do not add anything here for the harness itself; the harness
117
+ imports FROM the barrel (or directly from component source, orchestrator's/Codex's call — importing
118
+ from source is likely simpler and avoids a dist rebuild loop during dev) but nothing is added TO it.
119
+ - `tests/notificationsExports.test.js` — the existing exports-regression pattern to confirm still
120
+ passes untouched (proves the barrel didn't change).
121
+ - `src/notifications/realtime.jsx` — `useRealtimeCore({ active, wsUrl })` returns `{ subscribe }`;
122
+ `RealtimeContext` is the context the mock realtime provider must satisfy so components under it can
123
+ call `useRealtime()` without throwing. The harness's mock realtime adapter should provide a
124
+ `RealtimeContext.Provider` with an injectable `subscribe` (e.g. a manual event-dispatch harness) so
125
+ a harness entry can simulate a WS frame arriving, without opening a real socket.
126
+ - `src/notifications/NotificationsProvider.jsx` (read for the provider-wrapping pattern) — a
127
+ representative provider-consuming surface for the "at minimum one notifications surface" harness
128
+ entry requirement.
129
+ - `src/i18n/notificationsTranslations.ts` / `src/i18n/chartsTranslations.ts` — the flat i18n key
130
+ pattern; the harness needs `react-i18next` wired with at least these (or a minimal fixture set) so
131
+ mounted components render translated text instead of raw keys.
132
+ - Chart components (`ChartFrame`/`BarChart`/`LineChart`, per `tests/ChartFrame.test.jsx` etc.) — the
133
+ "at minimum one charts surface" harness entry.
134
+
135
+ **Invariants:**
136
+ - Zero new runtime dependencies (peer or direct) — Vite/plugin additions are dev-only.
137
+ - Harness is not exported, not built, not tested for behavior (only the three "does not affect the
138
+ product" assertions in Required Tests below).
139
+ - No change to the existing vitest test setup/config or any existing test file.
140
+ - No CI wiring — this is a local dev tool.
141
+
142
+ ### Required tests
143
+
144
+ Per envelope: extend/confirm the exports-regression test still passes with nothing new reachable from
145
+ it; a `build` succeeds and diffing `dist/` before/after shows no harness files and no content change;
146
+ inspect `package.json` to confirm `dependencies`/`peerDependencies` are byte-identical and only
147
+ `devDependencies` gained entries.
148
+
149
+ ### Progress contract
150
+
151
+ Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
152
+ `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (file opened, file
153
+ edited, command/test run) and `PROGRESS: [<n>/<total>] done` on step completion, spaced so no gap
154
+ exceeds ~2 min, stdout unbuffered, and exactly one final `RESULT: DONE|BLOCKED <reason>`.
155
+
156
+ ### Preamble (must be appended verbatim to the Codex prompt)
157
+
158
+ The text above is the COMPLETE spec — read nearest `AGENTS.md`, `.codex/skills/<role>/SKILL.md` (if
159
+ present), and this repo's `MEMORY.md` only for conventions; stay in scope; do not touch
160
+ `peerDependencies`/`dependencies`, only `devDependencies`; do not touch `src/index.js`'s exports; do
161
+ not update `MEMORY.md`; do NOT `git add`/`commit`/`push` — leave the change uncommitted in the working
162
+ tree for the orchestrator's independent review. WRITE and RUN the three required checks (exports test,
163
+ build + dist diff, package.json dependency diff) yourself to confirm — that is your only verification
164
+ run: do NOT run the full vitest suite as a "review" and do NOT run any review; the orchestrator does
165
+ both after you finish (though re-running `vitest run` in full is harmless and encouraged as a sanity
166
+ check since it's the existing test command, just don't treat it as satisfying the review step).
@@ -0,0 +1,375 @@
1
+ # MSG-3 — ucm messaging surfaces (full parity)
2
+
3
+ Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
4
+ **Binding spec:** `django-core-micha/docs/design/messaging-platform.md` — especially §ucm surface,
5
+ §REST contract, §Realtime, §Notification contract. On any conflict the design doc wins; deviations
6
+ from it are an operator scope change, never a silent edit.
7
+
8
+ Canonical register row: `django-core-micha/WORK_ORDERS.md` (the `MSG-*` workstream register). This
9
+ repo carries the ucm-side mirror row.
10
+
11
+ ---
12
+
13
+ ## Part A — Envelope (Expertenchat, 2026-07-31)
14
+
15
+ ### Goal
16
+
17
+ Build the shared messaging surfaces in ucm against the dcm messaging domain published in 2.36.1, so
18
+ that a consuming app gets a complete chat UI by mounting a provider and a few components, and supplies
19
+ only routing, display placement and scope pickers itself — never a forked state machine.
20
+
21
+ ### Two operator decisions that shape this WO (2026-07-31)
22
+
23
+ **1. Redesign is permitted — but no feature may be lost.** jg's current messaging UI is *not* a
24
+ verbatim visual target: where it grew organically across MSG-B1..B4, MSG-UX and MSG-DIALOG-1, MSG-3
25
+ may lay it out and structure the interaction better. The licence covers **layout, composition,
26
+ visual language and interaction flow**. It does **not** cover the feature set: the design's paper
27
+ test ("No jg feature is lost") stays binding, and dropping or degrading a capability is a scope
28
+ change back to the operator, not a design choice.
29
+
30
+ Because "redesign allowed" is easy to over-read, the WO carries a hard deliverable:
31
+ **a written deviation list** — every behavioural difference from jg's current messaging UI, with a
32
+ one-line rationale each, committed alongside the code (`docs/messaging-deviations.md` or an appendix
33
+ in this file). "We changed nothing behaviourally" is an acceptable list; an absent list is not.
34
+ Consequence to record now: **MSG-5 therefore becomes a visible change for jg users**, not a silent
35
+ migration, and needs its own UX review at adoption.
36
+
37
+ **2. Validation runs through the DX-1 dev harness.** ucm had no way to render anything; DX-1 adds a
38
+ Vite dev page. MSG-3 is expected to **use** it: every surface gets a harness entry, and the redesign
39
+ is iterated there rather than asserted in jsdom.
40
+
41
+ ### Expected outcome
42
+
43
+ Per design §ucm surface, a new `src/messaging/` subpackage mirroring the layout and conventions of the
44
+ existing `src/notifications/` subpackage (module layout, `src/i18n/<domain>Translations.ts`, flat
45
+ `tests/*.test.jsx`, additive barrel exports in `src/index.js` with an exports regression test):
46
+
47
+ - `MessagingProvider` + hook, an API adapter over the dcm REST contract, and a normalized cache.
48
+ - Realtime on **Layer 1**: `const { subscribe } = useRealtime()` — destructure it and depend on
49
+ `subscribe`, never on the recreated context object (design §Realtime states this explicitly; it is
50
+ a known footgun). Handle every frame the design lists, deduplicate by `event_id`, and refetch REST
51
+ state and cursors on reconnect. **No second socket, no client→server WS.**
52
+ - `ConversationList`, `Thread`, `Composer`, `ReadTicks`, `ReactionBar`, `PollCard`, `AttachmentList`,
53
+ config and per-conversation preference surfaces, and conversation launchers.
54
+ - Cursor pagination with infinite reverse scroll on thread history, from day one — the dcm contract
55
+ is opaque signed cursors, **not** offset paging.
56
+ - Optimistic send: the composer writes a local row keyed by `client_request_id`, reconciles the REST
57
+ response and the WS confirmation into exactly one row, and surfaces retry/error without duplicates.
58
+ - Labels, status text and validation messages in **de/en/fr**.
59
+ - Host apps supply routing, display placement and scope pickers; ucm supplies the state machine.
60
+
61
+ ### Parity inventory — the files that define "no feature lost"
62
+
63
+ The deviation list must be built against **all** of these, not just the obvious component folder:
64
+
65
+ | jg file | LOC | Note |
66
+ |---|---|---|
67
+ | `frontend/src/components/Messaging/Thread.jsx` | 2470 | the bulk: timeline, composer, replies, reactions, edit/delete, image upload, poll UI |
68
+ | `frontend/src/components/Messaging/ConversationList.jsx` | 166 | |
69
+ | `frontend/src/context/MessagingContext.jsx` | 184 | Layer-1 subscription + list/unread state |
70
+ | `frontend/src/components/Messaging/AnnouncementDialog.jsx` | 147 | broadcast/announcement composer incl. `link_target` deep-link |
71
+ | `frontend/src/components/Messaging/MessagingConfig.jsx` | 131 | per-scope config surface |
72
+ | `frontend/src/components/Messaging/NewDirectMessageDialog.jsx` | 103 | DM launcher — **the first-contact case MSG-2b unblocked** |
73
+ | `frontend/src/api/messagingApi.js` | 102 | |
74
+ | `frontend/src/components/Messaging/EmojiPickerButton.jsx` | 44 | |
75
+ | `frontend/src/components/Messaging/conversationHelpers.js` | 22 | |
76
+ | **`frontend/src/pages/MessagesPage.jsx`** | **310** | **easy to miss and NOT purely host-app routing** — see below |
77
+
78
+ ~3700 LOC excluding tests.
79
+
80
+ **`MessagesPage.jsx` needs an explicit split.** It is nominally jg's page, but it embeds behaviour that
81
+ belongs to this WO's "conversation launchers": the unified conversation+group list that surfaces
82
+ *unopened* groups as clickable launch items, and broadcast-conversation auto-surfacing/auto-open via
83
+ the `?tab=broadcast` query parameter. The page shell (routing, placement, master-detail layout) stays
84
+ with the host app per design §ucm surface; **the launcher behaviour it currently embeds is ucm scope**
85
+ and must appear in the deviation list either as reproduced or as an explicitly surfaced deviation.
86
+ This file was missing from the first parity inventory and is exactly the shape of gap through which a
87
+ feature disappears with nobody deciding to drop it.
88
+
89
+ ### Decomposition is a requirement, not a preference
90
+
91
+ jg's messaging UI is one 2470-LOC component with a 2173-LOC test file. **Reproducing that shape in ucm
92
+ is a scope violation, even if every feature is present.** The component list above is a set of binding
93
+ boundaries, not a suggested file layout. Concretely:
94
+
95
+ - **Each named component is a separately exported, independently mountable, independently testable
96
+ unit** — not an internal helper reachable only through `Thread`. `Composer`, `ReactionBar`,
97
+ `PollCard`, `AttachmentList` and `ReadTicks` must each stand on their own.
98
+ - **Domain state lives in the provider and its normalized cache; components read it through the
99
+ hook.** Design §ucm surface says host apps get components, "not a forked state machine" — the
100
+ corollary inside ucm is that subcomponents do not receive conversation/message state by
101
+ prop-drilling through `Thread`. This is the actual cause of a 2470-LOC component, not the feature
102
+ count. **This applies to domain state only** — conversations, messages, receipts, unread counts,
103
+ poll results. Ephemeral UI state stays local to the component that owns it: composer draft text,
104
+ upload progress, scroll and virtualization position, open menus, hover state. Lifting those into
105
+ the provider would be a worse design, not a more compliant one.
106
+ - **Every component gets its own DX-1 harness entry, mountable standalone.** This is the forcing
107
+ function: a monolith cannot be mounted piecewise, so the harness makes accidental re-monolithisation
108
+ visible immediately rather than at review time.
109
+ - **Soft size trigger:** any single component file above ~400 LOC must be justified explicitly in that
110
+ chunk's review. Not a hard cap — a deliberate, argued exception is fine, an unexamined 1500-line
111
+ component is not.
112
+ - `Thread` itself owns the timeline and its virtualization/scroll behaviour. Composing, reacting,
113
+ poll rendering, attachment rendering and receipt display are **collaborators it renders**, not
114
+ responsibilities it absorbs.
115
+
116
+ The `ui_reviewer` pass at WO end covers this explicitly, alongside design-system consistency.
117
+
118
+ ### Proposed chunk plan (staged commits, one independent review per chunk)
119
+
120
+ 1. `MessagingProvider` + API adapter + normalized cache + Layer-1 subscription (frames, `event_id`
121
+ dedup, reconnect refetch).
122
+ 2. `ConversationList` + launchers + unread / archive / mute + list pagination.
123
+ 3. `Thread` (timeline + scroll/virtualization only) + message rendering + one-level replies / thread
124
+ view + infinite reverse scroll, with `ReadTicks` as a separate collaborator component.
125
+ 4. `Composer` (optimistic send, retry/error) + attachment upload + `AttachmentList`.
126
+ 5. `ReactionBar` + `PollCard` + config/preferences + i18n sweep + barrel exports + the deviation list.
127
+
128
+ The register estimates 3–5 chunks. **Treat that as a floor, not a ceiling** — the jg parity target is
129
+ ~3700 LOC excluding tests, with `Thread.jsx` alone at 2470. If the work does not fit, split further
130
+ and say so; do not compress by dropping scope.
131
+
132
+ ### Required tests to WRITE (scoped per chunk; the reconciliation set is mandatory)
133
+
134
+ - **Optimistic send reconciliation:** a REST confirmation and a WS frame for the same
135
+ `client_request_id` produce exactly one message row — the classic duplicate-message bug.
136
+ - **`event_id` deduplication:** a repeated frame changes nothing.
137
+ - **Reconnect:** current list, open thread and unread count are refetched; no stale cursor is reused.
138
+ - **Pagination:** opaque cursor paging and reverse infinite scroll; a rejected cursor surfaces as an
139
+ error rather than an empty thread.
140
+ - **Read ticks:** aggregate state renders; per-recipient detail appears only where permitted and
141
+ **never for a direct conversation** — mirror the dcm carve-out on the client so the UI cannot imply
142
+ a capability the API refuses.
143
+ - **Attachment upload errors** surface as user-visible validation, not a generic failure (the MSG-2
144
+ chunk-4 lesson: a rejected upload must read as a rejection).
145
+ - **i18n coverage:** every new key exists in all three languages; no hardcoded user-facing string.
146
+ - **Exports regression:** the barrel exposes the new public surface, mirroring
147
+ `tests/notificationsExports.test.js`. This doubles as the decomposition check — every named
148
+ component must be individually exported and individually renderable in its own test, which a
149
+ monolith cannot satisfy.
150
+ - Existing ucm suites (notifications, onboarding, auth, charts) stay green — this WO is additive.
151
+
152
+ Per AGENTS.md "Test scope", per-chunk runs stay scoped to that chunk; the WO-end gate is the
153
+ affected-area set (messaging + notifications, since both share the Layer-1 transport).
154
+
155
+ ### Reviews
156
+
157
+ Beyond the standard independent `reviewer` per chunk, this WO explicitly carries **`ui_reviewer`** at
158
+ WO end — design-system consistency, reuse of existing ucm primitives, responsive behaviour, dark mode
159
+ and translation coverage. With a redesign licence in play, that pass is not optional.
160
+
161
+ ### Non-goals / do-not-touch
162
+
163
+ dcm changes (small sibling contract fixes are in scope per the design's release plan, but a
164
+ behavioural change to the dcm domain is not); any app-side code; jg's existing messaging UI (untouched
165
+ until MSG-5); the jg data migration; search; typing indicators; client→server WebSocket; scanner
166
+ infrastructure; `src/notifications/` behaviour beyond additive reuse — the Layer-1 primitive is
167
+ consumed, not modified.
168
+
169
+ ### Risks
170
+
171
+ - **Largest v1 block, built without a consumer.** MSG-5 is the first mount. The DX-1 harness is the
172
+ only feedback loop until then, and a mock adapter can drift from the real contract — shape fixtures
173
+ from the design doc's REST/realtime sections, not from imagination.
174
+ - **Redesign licence is the main scope risk.** Without the deviation list it silently becomes
175
+ "reimplement approximately", and jg loses features nobody decided to drop.
176
+ - **Optimistic send** is where duplicate/ghost messages come from; it has a mandatory test above.
177
+ - **`useRealtime` misuse** re-subscribes on every render if the context object is a dependency — a
178
+ documented footgun, called out in expected outcome.
179
+ - Estimate risk: see the chunk plan note.
180
+
181
+ ### Preconditions
182
+
183
+ dcm messaging published (2.36.1, incl. MSG-2b) · **DX-1 done** (the harness this WO validates
184
+ against) · Approval Gate #1 for MSG-3 = operator go on this envelope.
185
+
186
+ ### Release
187
+
188
+ One version bump + npm publish at WO end. No consumer pins are bumped here; MSG-5 picks the release
189
+ up via its own registry live-check before pinning.
190
+
191
+ ### Execution directive
192
+
193
+ Implement through `codex exec` in the background — invoked **directly via Bash** (never the
194
+ `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
195
+ `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
196
+ fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit. One
197
+ invocation per chunk, each chunk left uncommitted for the orchestrator's independent review.
198
+
199
+ ### Mini-handover (pastable)
200
+
201
+ Orchestrator: implement `work-orders/MSG-3.md` in `ui-core-micha` (main), chunk by chunk starting at
202
+ chunk 1. `git pull` first, read the WO + `django-core-micha/docs/design/messaging-platform.md`
203
+ (§ucm surface, §REST contract, §Realtime), then follow `orchestrate-codex` (Codex-first per chunk,
204
+ own review per chunk, `ui_reviewer` at WO end, one publish at WO end).
205
+
206
+ ---
207
+
208
+ ## Part B — Implementation map (Orchestrator)
209
+
210
+ ### Target repo / working directory
211
+
212
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo root; package `@micha.bigler/ui-core-micha`,
213
+ current published version 2.15.0). **Precondition: DX-1 must be landed and committed first** — do
214
+ not start this WO's chunk 1 until DX-1's commit exists on `main`.
215
+
216
+ ### Operator-approved exception: additive Layer-1 reconnect signal (2026-07-31)
217
+
218
+ Chunk 1's first dispatch found a real blocker: the design's "Reconnect refetches REST state/cursors"
219
+ (§Realtime) cannot be implemented against the existing Layer-1 primitive. `useRealtimeCore`
220
+ (`src/notifications/realtime.jsx`) tracks connect/backoff/reconnect internally but exposes only
221
+ `{ subscribe }` — no consumer, including `NotificationsProvider`'s own `RealtimeContext.Provider`
222
+ value, can observe a reconnect happening. This is confirmed by direct inspection, not assumed.
223
+
224
+ **Operator decision:** extend `src/notifications/realtime.jsx` and (if needed to thread the value
225
+ through) `src/notifications/NotificationsProvider.jsx` **additively** — e.g. an `onReconnect(callback)`
226
+ registration or a `connected` boolean added to the object `useRealtimeCore` returns and the
227
+ `RealtimeContext` value. This is the ONE approved exception to this WO's "consumed, not modified"
228
+ boundary for `src/notifications/`; do not use it as license to touch anything else there. Requirements
229
+ for the extension itself:
230
+ - Purely additive — existing consumers destructuring only `{ subscribe }` must be completely
231
+ unaffected (same behavior, same shape for the fields they read).
232
+ - No new WS consumer, no change to the connection/backoff logic itself, only exposing a signal that
233
+ already exists internally.
234
+ - Covered by its own test in the existing `tests/realtime.test.jsx` (or a new adjacent test file
235
+ following that convention) proving the reconnect signal actually fires when the internal `connect()`
236
+ path re-establishes after a close — not just that the field exists.
237
+ - This is chunk 1's first task, before the messaging-specific work; land it as a clearly-separated
238
+ part of chunk 1's diff (the reviewer will look at it as its own concern) rather than mixing it
239
+ invisibly into `MessagingProvider`'s implementation.
240
+
241
+ ### Shared context package (applies to every chunk)
242
+
243
+ **New subpackage to create:** `src/messaging/` — mirror `src/notifications/`'s layout and
244
+ conventions: flat files (`api.js`, `realtime.jsx` or reuse ucm's existing one — see below,
245
+ `MessagingProvider.jsx`, one file per named component), `src/i18n/messagingTranslations.ts` (flat
246
+ `{"Key.SUBKEY": {"de":…, "en":…, "fr":…}}` shape, matching `notificationsTranslations.ts`/
247
+ `chartsTranslations.ts`), flat `tests/*.test.jsx` (not nested under `tests/messaging/`), additive
248
+ barrel exports in `src/index.js` with an exports-regression test extending
249
+ `tests/notificationsExports.test.js`'s pattern (new file `tests/messagingExports.test.js` is fine,
250
+ or extend the existing one — Codex's call, consistency with existing convention matters more than
251
+ which file).
252
+
253
+ **Binding spec:** `django-core-micha/docs/design/messaging-platform.md` in full — §ucm surface,
254
+ §REST contract (exact endpoint list, payload shapes, cursor pagination), §Realtime (exact frame
255
+ names and payload shape — note MSG-2 chunk 3's fix: `message_deleted` carries ONLY
256
+ `message_id`/`deleted_at`/`deleted_by`, never content), §Notification contract (for awareness, not
257
+ implementation — ucm consumes `Notification`/`feed_visible=False` types via the existing
258
+ notifications surface, messaging doesn't re-implement notification delivery).
259
+
260
+ **Realtime — reuse, do not reinvent:** `src/notifications/realtime.jsx` already exports
261
+ `RealtimeContext`/`useRealtimeCore`/`useRealtime()`; `NotificationsProvider.jsx` is the sole mounter
262
+ of `RealtimeContext.Provider` (one WebSocket for the whole app). `MessagingProvider` must be mounted
263
+ **inside** an app's existing `NotificationsProvider` tree and call `useRealtime()` (throws if not
264
+ wrapped — see `realtime.jsx:13-19`) to get `{ subscribe }`. **Destructure `subscribe` and depend on
265
+ it in effects — never depend on the object `useRealtime()` returns**, which is recreated on every
266
+ `NotificationsProvider` render including unrelated bell-state changes (documented footgun, design
267
+ §Realtime, and jg's own `MessagingContext.jsx:32-35` comment explains exactly this bug). Messaging
268
+ does NOT open a second socket and does NOT export its own `RealtimeContext`/provider — it is a second
269
+ `subscribe(envelope, handler)` caller on the existing one, envelope key `"messaging"`.
270
+
271
+ **jg reference implementation (generalize, do not port verbatim — jg is event-scoped and
272
+ monolithic, ucm must be tenant-generic and decomposed per the envelope's decomposition requirement):**
273
+ - `../jg-ferien/frontend/src/context/MessagingContext.jsx` (184 lines, already read in full this
274
+ session) — provider shape precedent: WS handler switch on `data.type` (`message`,
275
+ `message_edited`, `message_deleted`, `poll_updated`) updating a conversation list + unread count;
276
+ `activeIdRef` pattern to avoid stale-closure issues in the WS handler; `markConversationRead`
277
+ optimistic local update before/alongside the REST call. ucm's version must be scope/tenant-generic
278
+ (no `eventId` parameter baked in — host app supplies scope/filter params) and must normalize into
279
+ a proper cache (conversations, messages, threads, polls, reactions as separate normalized slices
280
+ keyed by id), not just a flat conversation array — chunks 2-5 need to read/write messages and
281
+ thread state independently of the conversation list.
282
+ - `../jg-ferien/frontend/src/api/messagingApi.js` (102 lines, already read in full) — REST call
283
+ shape precedent (axios wrapper over `apiClient`). ucm's adapter targets the NEW dcm paths from the
284
+ design doc (`/api/messaging/conversations/`, `/api/messaging/conversations/direct/`, etc.) — these
285
+ differ from jg's current paths (jg's own adoption/migration is MSG-5, a separate WO; do not target
286
+ jg's current legacy paths, target the published dcm 2.36.1 contract in the design doc).
287
+ - `../jg-ferien/frontend/src/context/realtimeEnvelopes.js` — `MESSAGING_ENVELOPE = "messaging"`, the
288
+ envelope discriminator string ucm's `subscribe("messaging", handler)` call must use (matches the
289
+ dcm design doc's realtime frame `envelope: 'messaging'` field).
290
+ - `src/notifications/NotificationsProvider.jsx` (already read in part) — `normalizeNotification`-style
291
+ normalization pattern, `useCallback`/`useRef` conventions, how it composes `AuthContext` + realtime
292
+ + REST fetch into one provider. Use as the shape precedent for `MessagingProvider`, not as a literal
293
+ template (notifications is a flat feed; messaging needs a genuinely normalized multi-entity cache).
294
+
295
+ **Invariants (all chunks):**
296
+ - No app-specific code — no event/scope assumptions baked into components; scope/filter params are
297
+ passed in by the host app (design §ucm surface: "host apps supply routing, display placement and
298
+ scope pickers").
299
+ - Every named component (`ConversationList`, `Thread`, `Composer`, `ReadTicks`, `ReactionBar`,
300
+ `PollCard`, `AttachmentList`) is independently exported and independently mountable — verify via
301
+ its own DX-1 harness entry per the envelope's decomposition requirement, not just via `Thread`
302
+ rendering it internally.
303
+ - Domain state (conversations, messages, receipts, unread counts, poll results) lives in the
304
+ provider's normalized cache; components read via the hook. Ephemeral UI state (draft text, upload
305
+ progress, scroll position, open menus) stays component-local — do not lift it into the provider.
306
+ - Cursor pagination is opaque and signed (matches dcm's `django.core.signing`-based cursor) — never
307
+ build an offset/page-number UI; a bad/tampered cursor from the API is a 400, surface it as an
308
+ error state, not an empty list.
309
+ - `client_request_id`-keyed optimistic send: composer writes a local pending row immediately,
310
+ reconciles when the REST response and/or the WS `message` frame for the same id arrives — must
311
+ converge to exactly one row, not two.
312
+ - i18n: every user-facing string goes through `messagingTranslations.ts` in all three languages; no
313
+ hardcoded text.
314
+ - No client→server WebSocket path anywhere — every write is REST, even optimistic ones (the socket
315
+ is read-only fan-out).
316
+
317
+ ### Chunk 1 — this dispatch
318
+
319
+ **Scope:** `MessagingProvider` + hook (`useMessaging()`) + API adapter (`src/messaging/api.js`) +
320
+ normalized cache + Layer-1 subscription (frame handling for at least `message`, `message_edited`,
321
+ `message_deleted`, `conversation_upsert`, `participant_changed`; the remaining frame types
322
+ `reaction`/`poll_updated`/`delivered`/`read_state`/`thread_read_state`/`attachment_ready`/
323
+ `conversation_archived` can have handler stubs wired now and filled in by the chunk that owns that
324
+ feature — e.g. `poll_updated` real handling lands in chunk 5 with `PollCard`, but the dedup/dispatch
325
+ plumbing must exist now so later chunks only add a case, not restructure the subscriber). `event_id`
326
+ deduplication (a `Set` of recently-seen ids, per design's "handlers deduplicate `event_id`").
327
+ Reconnect refetch (on the realtime connection re-establishing, refetch current conversation list +
328
+ open thread + unread count — jg has no precedent for this since it never handles reconnect
329
+ explicitly; this is a v1 requirement per design §Realtime "Reconnect refetches REST state/cursors").
330
+
331
+ **No components in this chunk** — `ConversationList`/`Thread`/etc. are chunks 2-5. This chunk's DX-1
332
+ harness entry (if any) is a minimal "provider + raw state dump" page proving the provider mounts,
333
+ fetches, and updates on a simulated WS frame — full component harness entries land with their
334
+ owning chunks.
335
+
336
+ **Required tests for this chunk:**
337
+ - `event_id` deduplication: a repeated frame changes nothing (mandatory per envelope).
338
+ - Reconnect: refetch is triggered (mock the realtime reconnect signal — check `useRealtimeCore`/DX-1
339
+ harness's mock adapter for how a reconnect is simulated; if the mock adapter can't yet simulate a
340
+ reconnect, that's a DX-1 gap to note, not something to silently skip).
341
+ - API adapter: each REST call targets the correct dcm path/method per the design doc's §REST table
342
+ (a scoped unit test per function is fine, doesn't need a live backend).
343
+ - Normalized cache: applying a `message` frame updates only the affected conversation's/message's
344
+ slice, not the whole cache (proves normalization, not just "state changed").
345
+ - Exports regression: `MessagingProvider`/`useMessaging` reachable from the barrel.
346
+
347
+ ### Progress contract (every chunk)
348
+
349
+ Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
350
+ `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (file opened, file
351
+ edited, command/test run) and `PROGRESS: [<n>/<total>] done` on step completion, spaced so no gap
352
+ exceeds ~2 min, stdout unbuffered, and exactly one final `RESULT: DONE|BLOCKED <reason>` per chunk.
353
+
354
+ ### Preamble (append verbatim to every chunk's Codex prompt)
355
+
356
+ The text above is the COMPLETE spec for this chunk — read nearest `AGENTS.md`,
357
+ `.codex/skills/<role>/SKILL.md` (if present), and this repo's `MEMORY.md` only for conventions; stay
358
+ in scope; do not touch anything outside `src/messaging/`, `src/i18n/messagingTranslations.ts`, the
359
+ named test files, and additive `src/index.js` barrel exports; do not touch
360
+ `peerDependencies`/`dependencies` (only this WO's own runtime needs, if any, and only with the
361
+ orchestrator's awareness — flag before adding anything beyond what's already a peer/dev dependency);
362
+ do not update `MEMORY.md`; do NOT `git add`/`commit`/`push` — leave the chunk's changes uncommitted
363
+ for the orchestrator's independent review. WRITE this chunk's required tests AND RUN them to confirm
364
+ they pass — that is your only test run: do NOT run the full ucm suite and do NOT run any review; the
365
+ orchestrator runs the affected-area set and the independent review after you finish. Use the DX-1 dev
366
+ harness (`dev/` or wherever DX-1 placed it) to visually verify anything you build that renders —
367
+ add/update a harness entry for this chunk's surface even if the required tests above don't mention it
368
+ explicitly, per the envelope's "every surface gets a harness entry" requirement.
369
+
370
+ ### Mini-handover (pastable, per chunk)
371
+
372
+ Orchestrator: implement chunk <n> of `work-orders/MSG-3.md` in `ui-core-micha` (main). `git pull`
373
+ first, read the WO + `django-core-micha/docs/design/messaging-platform.md`, then follow
374
+ `orchestrate-codex` (Codex-first, own independent review per chunk, `ui_reviewer` at WO end, one
375
+ publish at WO end).