@micha.bigler/ui-core-micha 3.14.3 → 4.0.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.
- package/CHANGELOG.md +31 -0
- package/README.md +7 -0
- package/dist/auth/UserMenu.js +1 -1
- package/dist/auth/apiClient.js +7 -6
- package/dist/chat/ChatList.js +1 -1
- package/dist/chat/ChatThread.js +2 -1
- package/dist/chat/ChatWorkspace.js +1 -1
- package/dist/chat/ModelPicker.js +2 -5
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/chat/useChatSocket.js +1 -1
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/SecurityComponent.js +1 -1
- package/dist/components/UserListComponent.js +2 -8
- package/dist/components/charts/ChartFrame.js +1 -1
- package/dist/components/charts/ScatterChart.js +2 -1
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/layout/SectionNav.js +1 -1
- package/dist/messaging/AttachmentList.js +2 -2
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/ConversationLaunchers.js +1 -4
- package/dist/messaging/ConversationList.js +1 -1
- package/dist/messaging/DirectMessageLauncher.js +1 -1
- package/dist/messaging/MessagingProvider.js +0 -0
- package/dist/messaging/PollCard.js +7 -5
- package/dist/messaging/ReadTicks.js +2 -2
- package/dist/messaging/Thread.js +5 -3
- package/dist/notifications/NotificationBell.js +4 -7
- package/dist/notifications/NotificationSettings.js +1 -1
- package/dist/onboarding/OnboardingProvider.js +3 -6
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +2 -3
- package/dist/pages/PasswordChangePage.js +2 -3
- package/dist/pages/PasswordInvitePage.js +2 -3
- package/dist/pages/PasswordResetRequestPage.js +2 -3
- package/dist/pages/SignUpPage.js +2 -3
- package/dist/pages/SignupConfirmPage.js +2 -3
- package/dist/theme/createAppTheme.js +5 -4
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/dist/theme/themeCompleteness.js +3 -12
- package/dist/utils/auth-errors.js +4 -3
- package/package.json +41 -16
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -148
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
package/work-orders/MSG-6e.md
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
# WORK ORDER MSG-6e (ui-core-micha) — messaging surface defects found by click-through
|
|
2
|
-
|
|
3
|
-
**EXECUTION DIRECTIVE.** If you are the implementer reading this as your own spec, this section is not
|
|
4
|
-
addressed to you — it tells the Orchestrator how to invoke you; you ARE that invocation, do not shell
|
|
5
|
-
out to `codex exec`. Orchestrator: implement through `codex exec` in the background, invoked **directly
|
|
6
|
-
via Bash** (never the `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check`
|
|
7
|
-
and `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from this
|
|
8
|
-
file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
|
|
9
|
-
|
|
10
|
-
Feature-level WO (Expertenchat envelope). Origin: jg-ferien `work-orders/MSG-9.md` findings 3, 7, 9, 10,
|
|
11
|
-
13b and 14 — all reproduced in a running app on 2026-08-03. Read that file's "Live verification"
|
|
12
|
-
section; do not re-derive the evidence.
|
|
13
|
-
|
|
14
|
-
## TIER
|
|
15
|
-
Tier 2 — shared-core, consumed by every app. Independent `reviewer` mandatory. `ui_reviewer` requested
|
|
16
|
-
for scope A (visible layout change).
|
|
17
|
-
|
|
18
|
-
## SCOPE — six independent defects, one release
|
|
19
|
-
|
|
20
|
-
**A. Timestamps: `HH:MM` plus a day separator, and pass the active locale (findings 3 + 9).**
|
|
21
|
-
`MessageBubble.jsx:133` calls `new Date(message.created_at).toLocaleString()` **with no locale
|
|
22
|
-
argument**, so it renders in the runtime's default locale — observed as `8/3/2026, 10:41:29 AM` in a
|
|
23
|
-
German UI. Two changes, and they must land together:
|
|
24
|
-
|
|
25
|
-
1. Show `HH:MM` on the bubble. The **date** appears once as a separator row when the day changes,
|
|
26
|
-
inserted by `Thread` (it needs the neighbouring message to compare against), **not** by
|
|
27
|
-
`MessageBubble`.
|
|
28
|
-
2. Pass i18next's active language to every date formatter. `ConversationList` already threads
|
|
29
|
-
`i18n?.language` into `relativeTime` — follow that existing pattern rather than inventing another.
|
|
30
|
-
|
|
31
|
-
Not a one-off: `SupportRecoveryRequestsTab.jsx:368` and `:410` have the same bare `toLocaleString()`.
|
|
32
|
-
Fix them in the same pass; leaving them is how this recurs.
|
|
33
|
-
|
|
34
|
-
**B. A freshly created poll is cached under the wrong id (finding 10).**
|
|
35
|
-
`createConversationPoll` (`MessagingProvider.jsx:401-406`):
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
const message = poll?.message || poll; // <- dcm never sends `message`
|
|
39
|
-
if (message?.id != null) patchMessage({ ...message, ..., poll: ... });
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
dcm's `_poll_response` (`messaging/views.py:32-36`) returns `serialize_poll(poll)` + `voted_option_ids`.
|
|
43
|
-
Keys: `id`, `question`, `allow_multiple`, `closed_at`, `created_by_id`, `options`, `voted_option_ids`.
|
|
44
|
-
**No `message`, no `message_id`.** So `message.id` is the **poll's** id and `patchMessage` writes a
|
|
45
|
-
phantom timeline entry keyed by it, with no `created_at`, no `sender_id`, no `kind`.
|
|
46
|
-
|
|
47
|
-
Observed: a newly created poll renders with **no timestamp**, sender "Unbekannte Person", and sorts to
|
|
48
|
-
the **top** of the timeline (`chronological()` on `new Date(undefined)` → `NaN`), above messages from
|
|
49
|
-
two days earlier. Correct after reload.
|
|
50
|
-
|
|
51
|
-
**The `|| poll` fallback is the actual defect** — it converts a wrong assumption into silent data
|
|
52
|
-
corruption instead of an error. Remove the guess: resolve the message id explicitly from a field dcm
|
|
53
|
-
really sends, or re-fetch the message. **If dcm's response genuinely carries no way to identify the
|
|
54
|
-
message, do not invent one — stop and report**; that would be a dcm contract gap needing its own WO.
|
|
55
|
-
Apply the same scrutiny to `castPollVote`/`closePoll` (`:407+`), which use the same response shape.
|
|
56
|
-
|
|
57
|
-
**C. Handle the read-state frames dcm already sends (finding 7).**
|
|
58
|
-
`applyFrame` (`MessagingProvider.jsx:123ff`) handles only `message`, `message_edited`, `reaction` and
|
|
59
|
-
`poll_updated`. dcm publishes `read_state` (`services.py:293`), `delivered` (`:302`) and
|
|
60
|
-
`thread_read_state` (`:312`) — all silently discarded. Combined with `ReadTicks` fetching its status
|
|
61
|
-
once on mount (`ReadTicks.jsx:15-19`) and never re-fetching, a receipt can **never** update within a
|
|
62
|
-
session.
|
|
63
|
-
|
|
64
|
-
**Ordering, updated 2026-08-03 — the earlier caveat here is resolved.** jg MSG-9 finding 1 is now
|
|
65
|
-
diagnosed as `django-core-micha` MSG-8: `push_to_users` feeds raw `datetime` objects into a msgpack
|
|
66
|
-
channel layer, `group_send` raises, the exception is swallowed as a warning, and the frame is dropped
|
|
67
|
-
(measured 16/16 failures). **C is therefore worth doing and no longer speculative.**
|
|
68
|
-
|
|
69
|
-
Two concrete implications:
|
|
70
|
-
|
|
71
|
-
- `read_state`, `delivered` and `thread_read_state` build their payloads with an explicit
|
|
72
|
-
`.isoformat()`, so **those three frames arrive today** — C is verifiable right now, before MSG-8
|
|
73
|
-
lands.
|
|
74
|
-
- `message` / `message_edited` / `conversation_upsert` frames start arriving only once MSG-8 ships. Do
|
|
75
|
-
not conclude from "no message frame appears" that C is broken; check MSG-8's status first.
|
|
76
|
-
|
|
77
|
-
**D. Stop double-firing `markConversationRead` (finding 13, ucm half).**
|
|
78
|
-
Both `Thread.jsx:55` (a `useEffect` on `conversationId`) and `ConversationList.jsx:77` (the row's
|
|
79
|
-
`onClick`) call it for the same conversation. Measured: `POST /conversations/<id>/read/` fires **twice**
|
|
80
|
-
on every open. Keep exactly one owner. `Thread` is the better one — it fires on every way a
|
|
81
|
-
conversation becomes active, including deep links and programmatic switches, whereas the list click
|
|
82
|
-
covers only one path. Removing the `Thread` effect instead would silently break those paths.
|
|
83
|
-
|
|
84
|
-
**E. Plural forms for the reply counter (finding 14).**
|
|
85
|
-
`MessagingThread.SHOW_REPLIES` is a single string `'{{count}} Antworten anzeigen'`
|
|
86
|
-
(`i18n/messagingTranslations.ts:32`). All three languages are wrong at count 1: "1 Antworten anzeigen",
|
|
87
|
-
"Show 1 replies", "1 réponses". Use i18next plural keys (`_one`/`_other`). **Audit the rest of
|
|
88
|
-
`messagingTranslations.ts` for other `{{count}}` strings without plural forms** and fix them in the same
|
|
89
|
-
pass — including `MessagingReadTicks.DELIVERED` if it survives dcm MSG-7 scope C.
|
|
90
|
-
|
|
91
|
-
## DEPENDENCY — do not fix findings 2, 5 and 11 here
|
|
92
|
-
Own/incoming alignment, missing read ticks and "Unbekannte Person" all trace to dcm never serializing a
|
|
93
|
-
`sender` object. **ucm's code is already correct for all three** — `MessageBubble.jsx:100/108/109`
|
|
94
|
-
implements the alignment in full, it simply never sees `isOwn === true`. That fix is
|
|
95
|
-
`django-core-micha` MSG-7. **Changing `MessageBubble`'s ownership logic here would be wrong and would
|
|
96
|
-
mask the real defect.** If a change in this WO appears to need it, stop and report.
|
|
97
|
-
|
|
98
|
-
## NON-GOALS / DO NOT TOUCH
|
|
99
|
-
- No change to `isOwn`, `canShowReadTicks`, `senderName`, or any ownership derivation (see above).
|
|
100
|
-
- No change to `reconcileMessage`'s merge semantics — the optimistic-`sender` survival it produces is
|
|
101
|
-
currently the only thing making own messages render at all; changing it before MSG-7 lands would make
|
|
102
|
-
the surface strictly worse.
|
|
103
|
-
- No change to `useRealtimeCore`. The missing heartbeat is real (jg MSG-9 finding 1) but is its own
|
|
104
|
-
Tier 0 diagnosis with cross-app scope.
|
|
105
|
-
- No new dependencies.
|
|
106
|
-
|
|
107
|
-
## RISKS
|
|
108
|
-
- **B is the one that can corrupt state.** A wrong id key writes a phantom row into every consumer's
|
|
109
|
-
timeline. Get its test right before the fix.
|
|
110
|
-
- A changes visible layout in every host app — hence `ui_reviewer`.
|
|
111
|
-
- D: removing the wrong caller silently breaks read-marking on deep links. State which one you kept and
|
|
112
|
-
why.
|
|
113
|
-
- E: i18next plural resolution depends on the configured language; a key that works in `de` can still be
|
|
114
|
-
wrong in `fr`. Assert all three.
|
|
115
|
-
|
|
116
|
-
## REQUIRED TESTS TO WRITE
|
|
117
|
-
Narrow and behavioural. Do NOT run the full suite.
|
|
118
|
-
|
|
119
|
-
1. **B, the important one:** creating a poll puts exactly **one** new entry in the message cache, keyed
|
|
120
|
-
by the **message** id, carrying `created_at`, and it sorts **last** in a timeline whose other
|
|
121
|
-
messages are older. Assert the poll id is **not** used as a message key.
|
|
122
|
-
2. A bubble renders `HH:MM` only; a day separator appears exactly once between two messages on different
|
|
123
|
-
days and **not** between two on the same day.
|
|
124
|
-
3. Date formatting uses the active i18next language — assert a `de` render differs from an `en` render
|
|
125
|
-
for the same timestamp (this fails today, where both produce the runtime default).
|
|
126
|
-
4. D: opening a conversation issues exactly **one** `markConversationRead` call, via both entry paths
|
|
127
|
-
(list click and direct mount).
|
|
128
|
-
5. E: `SHOW_REPLIES` at `count: 1` renders the singular in **de, en and fr**.
|
|
129
|
-
6. C: a `read_state` frame and a `thread_read_state` frame each update the cached receipt `ReadTicks`
|
|
130
|
-
actually renders, without a REST round-trip. No test asserts a `delivered` frame does anything — it
|
|
131
|
-
should not (dcm MSG-7 retires that frame's only publisher in this same run).
|
|
132
|
-
|
|
133
|
-
**Non-vacuity:** each test must fail with its fix reverted. Test 1 in particular must fail against
|
|
134
|
-
today's `|| poll` fallback — if it passes on the unmodified code, it is not testing the defect.
|
|
135
|
-
|
|
136
|
-
## TEST SCOPE FOR THE GATE (orchestrator)
|
|
137
|
-
The messaging test files plus whatever covers `SupportRecoveryRequestsTab`. Not the full suite.
|
|
138
|
-
|
|
139
|
-
## OPERATOR DECISIONS (this run)
|
|
140
|
-
- **Scope C — superseded below.** The envelope above was updated after the original defer decision was
|
|
141
|
-
made (finding 1 is now diagnosed as `django-core-micha` MSG-8, not still-open); the instruction that
|
|
142
|
-
follows replaces the earlier "defer all of C" call.
|
|
143
|
-
- **Implement C narrowly: `read_state` and `thread_read_state` only. Do NOT add a `delivered` frame
|
|
144
|
-
handler.** The envelope's update confirms these three frames already carry an explicit `.isoformat()`
|
|
145
|
-
and arrive today, independent of MSG-8 (which only gates `message`/`message_edited`/`conversation_upsert`).
|
|
146
|
-
But `delivered` is the one exception: dcm MSG-7 (this same run, scope C there) **deletes `mark_delivered`**
|
|
147
|
-
— the only production code that ever published a `delivered` frame — as part of retiring delivery
|
|
148
|
-
tracking down to an honest two-state receipt (`all_read` only). Adding a `delivered` handler in
|
|
149
|
-
`applyFrame` here would be live-testable today (against the pre-MSG-7 backend) but dead on arrival the
|
|
150
|
-
moment MSG-7 ships, and contradicts the product decision behind it. In `applyFrame`
|
|
151
|
-
(`MessagingProvider.jsx:123ff`), add `read_state`/`thread_read_state` branches that merge the received
|
|
152
|
-
`last_read_at` (and for `thread_read_state`, the `root_id`) into the relevant cached receipt/message so
|
|
153
|
-
`ReadTicks`/thread-unread state updates without a REST round-trip — do not add a third branch for
|
|
154
|
-
`delivered`. Required test 6 covers `read_state`; add the equivalent for `thread_read_state`, do not add
|
|
155
|
-
one for `delivered`.
|
|
156
|
-
- **Scope B's root cause is now confirmed and fixed at the source, in parallel, by `django-core-micha`
|
|
157
|
-
MSG-7 scope F (operator-approved extension, running concurrently in this session):** dcm's poll REST
|
|
158
|
-
response (`_poll_response` / `serialize_poll`) previously had no `message_id` at all — verified by
|
|
159
|
-
reading dcm's `views.py`/`serializers.py`/`models.py` directly, not assumed. It now returns
|
|
160
|
-
`message_id` (a string, dcm's UUID-as-string convention) on create/vote/close. **Do not re-derive or
|
|
161
|
-
re-verify dcm's fix — build against the assumption that `poll.message_id` is present** on the response
|
|
162
|
-
object `createConversationPoll`/`castPollVote`/`closeConversationPoll` receive. If your local dcm
|
|
163
|
-
checkout does not yet have `message_id` on that response when you test, that is a sequencing problem to
|
|
164
|
-
report, not a signal to invent a fallback in ucm.
|
|
165
|
-
|
|
166
|
-
## IMPLEMENTATION MAP (Orchestrator)
|
|
167
|
-
|
|
168
|
-
### Context package
|
|
169
|
-
|
|
170
|
-
**A — timestamps (`src/messaging/MessageBubble.jsx`, `src/components/SupportRecoveryRequestsTab.jsx`)**
|
|
171
|
-
- `MessageBubble.jsx:133` — `{new Date(message.created_at).toLocaleString()}` inside the `hasMeta` block
|
|
172
|
-
(`:132-136`). Change to render `HH:MM` only, with the active i18next locale, e.g.
|
|
173
|
-
`new Date(message.created_at).toLocaleTimeString(i18n.language, { hour: '2-digit', minute: '2-digit' })`
|
|
174
|
-
— `useTranslation()` is already destructured as `{ t }` at `:33`; add `i18n` to that destructure.
|
|
175
|
-
- The day-separator is `Thread`'s job (it has the neighbouring message), not `MessageBubble`'s. In
|
|
176
|
-
`Thread.jsx`, the render loop over `roots` (`:139-153`, `roots.map((message) => { ... })`) and the
|
|
177
|
-
nested `replies.map` (`:151`) only ever see one message at a time inside the callback — you need the
|
|
178
|
-
previous rendered root's date to compare against. Track it across iterations (e.g. reduce over `roots`
|
|
179
|
-
building an array of `{ message, showDateSeparator }` before rendering, rather than mutating a ref
|
|
180
|
-
inside `.map`, which fires render-order-dependent and is fragile under React StrictMode's double-invoke).
|
|
181
|
-
Insert a separator row (a `Divider`-adjacent `Typography` with the formatted date, locale-aware, e.g. via
|
|
182
|
-
`toLocaleDateString(i18n.language)`) when the day changes. `chronological()` (`Thread.jsx:10`) already
|
|
183
|
-
guarantees ascending order, so a simple previous-vs-current day comparison suffices — no need to
|
|
184
|
-
re-sort.
|
|
185
|
-
- `ConversationList.jsx`'s `relativeTime(value, locale)` (`:31-36`) is the existing pattern for threading
|
|
186
|
-
`i18n?.language` through a date formatter (called at `:89` with `i18n?.language`) — follow that shape,
|
|
187
|
-
don't invent a second convention.
|
|
188
|
-
- `SupportRecoveryRequestsTab.jsx:368` and `:410` (both bare `new Date(...).toLocaleString()`) — same fix,
|
|
189
|
-
same locale-threading. Check whether that component already has `i18n` from `useTranslation()`; if not,
|
|
190
|
-
destructure it there too.
|
|
191
|
-
|
|
192
|
-
**B — poll cache key (`src/messaging/MessagingProvider.jsx`)**
|
|
193
|
-
- `createConversationPoll` (`:401-406`): replace `const message = poll?.message || poll;` — dcm never
|
|
194
|
-
sends a `message` object, only the flat poll projection now carrying `message_id` (see Operator
|
|
195
|
-
Decisions above). Resolve the message id from `poll.message_id` explicitly; if absent, treat it as a
|
|
196
|
-
real error (don't silently fall back to `poll.id`). Something like:
|
|
197
|
-
```js
|
|
198
|
-
const messageId = poll?.message_id;
|
|
199
|
-
if (messageId != null) patchMessage({ id: messageId, conversation_id: poll.conversation_id ?? conversationId, created_at: poll.created_at ?? new Date().toISOString(), poll });
|
|
200
|
-
```
|
|
201
|
-
— note dcm's poll object itself carries no `created_at` either (check `serialize_poll`); you likely need
|
|
202
|
-
to accept that a freshly created poll message has no server `created_at` until the next fetch/frame, OR
|
|
203
|
-
confirm whether the realtime `poll_updated` frame (which does carry a full conversation/message context
|
|
204
|
-
via `_poll_updated_payload`) arrives fast enough in practice to backfill it. Investigate `serialize_poll`'s
|
|
205
|
-
actual returned keys yourself (do not assume) and choose the least-guessy option; state your reasoning.
|
|
206
|
-
- `castPollVote` (`:407-412`) / `closeConversationPoll` (`:413-418`) use `cache.messages[messageId]` where `messageId` is already
|
|
207
|
-
passed in by the caller (not derived from the poll response) — these two are likely already correct
|
|
208
|
-
today (the bug is specific to *creation*, where the id doesn't exist in cache yet). Verify, don't assume;
|
|
209
|
-
the WO's instruction to apply "the same scrutiny" to these means check-first, not change-by-default.
|
|
210
|
-
|
|
211
|
-
**D — double `markConversationRead` (`src/messaging/Thread.jsx`, `src/messaging/ConversationList.jsx`)**
|
|
212
|
-
- `Thread.jsx:56` — `useEffect(() => { if (conversationId != null) markConversationRead(conversationId)...`
|
|
213
|
-
- `ConversationList.jsx:85` — the `ListItemButton`'s `onClick` also calls `markConversationRead(...)`.
|
|
214
|
-
- Keep `Thread.jsx:56`; remove the call from `ConversationList.jsx:85`'s `onClick` (keep `onOpen?.(conversation)`
|
|
215
|
-
itself — only drop the `markConversationRead` call and its `.catch`).
|
|
216
|
-
|
|
217
|
-
**C — wiring gap you must close, not just `applyFrame` (`src/messaging/MessagingProvider.jsx`, `src/messaging/ReadTicks.jsx`)**
|
|
218
|
-
- `EMPTY_CACHE` (`:35`) already declares a `receipts: {}` slot — unused today, no reducer case writes to
|
|
219
|
-
it and nothing reads from it. This is almost certainly the intended home for what `read_state`/
|
|
220
|
-
`thread_read_state` frames should update. Add reducer cases in `applyFrame` (`:123ff`, alongside the
|
|
221
|
-
existing `message`/`reaction`/`poll_updated`/etc. branches) for `frame.type === 'read_state'` and
|
|
222
|
-
`'thread_read_state'` that merge into `state.receipts`, keyed so `ReadTicks` can look itself up (e.g. by
|
|
223
|
-
`conversation_id` + `user_id` for `read_state`, by `root_id` + `user_id` for `thread_read_state` — the
|
|
224
|
-
payload shapes are `{"user_id", "last_read_at"}` and `{"user_id", "root_id", "last_read_at"}`
|
|
225
|
-
respectively, per dcm `services.py:293`/`:312`).
|
|
226
|
-
- **Writing to `cache.receipts` alone does not satisfy required test 6** ("updates the cached receipt
|
|
227
|
-
without a REST round-trip") unless something actually reads it. `ReadTicks.jsx` (`:10-19`) today only
|
|
228
|
-
ever calls `getMessageReadStatus(messageId)` once on mount via `useEffect`, into its own local `status`
|
|
229
|
-
state — it has no path back to `cache` at all. You need `ReadTicks` to react to the relevant slice of
|
|
230
|
-
`cache.receipts` (via `useMessaging()`) so a frame arriving after the initial REST fetch actually updates
|
|
231
|
-
what's rendered — e.g. merge a fresher `cache.receipts` entry over the REST-derived `status` state, or
|
|
232
|
-
derive `all_read` from the participant set + `cache.receipts` entries you have instead of holding a
|
|
233
|
-
second independent read of the same fact. Use your judgment on the exact merge (a full redesign of
|
|
234
|
-
`ReadTicks`' data source is not required — the REST fetch stays as the initial/fallback source of truth,
|
|
235
|
-
e.g. for `recipient_detail`, which no frame carries), but the frame update must be observable in what
|
|
236
|
-
`ReadTicks` renders, not just in an otherwise-unread part of `cache`.
|
|
237
|
-
|
|
238
|
-
**E — plural forms (`src/i18n/messagingTranslations.ts`)**
|
|
239
|
-
- `MessagingThread.SHOW_REPLIES` (`:32`): convert to i18next `_one`/`_other` keys (de/en/fr), all three —
|
|
240
|
-
`Thread.jsx:144` already calls `t('MessagingThread.SHOW_REPLIES', { count: ... })`, i18next resolves the
|
|
241
|
-
plural key automatically once `_one`/`_other` exist; no call-site change needed.
|
|
242
|
-
- Audit found two more `{{count}}` strings with no plural variants: `MessagingComposer.FILES_SELECTED`
|
|
243
|
-
(`:64`) and `MessagingReactions.TOGGLE` (`:72`) — fix both the same way. `MessagingPoll.OPTION` (`:85`,
|
|
244
|
-
`"Option {{count}}"`) is an ordinal label, not a plural noun phrase — it reads correctly at every count
|
|
245
|
-
in all three languages as-is; leave it alone.
|
|
246
|
-
- `MessagingReadTicks.DELIVERED` (`:57`) — **do not add plural forms for it.** Per the WO's own conditional
|
|
247
|
-
("if it survives dcm MSG-7 scope C"): it does not survive — dcm MSG-7 removes `delivered_count` from
|
|
248
|
-
`read_status` this same run (operator-approved), so `ReadTicks.jsx:21`'s
|
|
249
|
-
`status.delivered_count || 0` will render `0` from now on for a different reason (`undefined` instead of
|
|
250
|
-
an always-NULL field) but with **no change in rendered output** — it already always showed "0" before
|
|
251
|
-
this run. This is a pre-existing, unrelated-to-this-WO UX rough edge (a two-state receipt showing a
|
|
252
|
-
meaningless count); flag it as a follow-up in your final report, do not fix `ReadTicks.jsx` here — that
|
|
253
|
-
is out of this WO's scope A-E.
|
|
254
|
-
|
|
255
|
-
### Do-not-touch reminders (from the envelope, restated)
|
|
256
|
-
No change to `isOwn`/`canShowReadTicks`/`senderName`/ownership derivation (that's dcm MSG-7's job) or to
|
|
257
|
-
`reconcileMessage`'s merge semantics or `useRealtimeCore`.
|
|
258
|
-
|
|
259
|
-
### Target repo working directory
|
|
260
|
-
`C:\Users\biglmi\Documents\webapps\ui-core-micha`
|
|
261
|
-
|
|
262
|
-
## TARGET REPO
|
|
263
|
-
`C:\Users\biglmi\Documents\webapps\ui-core-micha`. Branch `main` (or `develop` if present). Publish per
|
|
264
|
-
the repo's release flow; **jg's pin bump is a separate step and NOT part of this WO** — jg also needs
|
|
265
|
-
django-core-micha MSG-7, and bumping one without the other leaves the surface half-fixed.
|
|
266
|
-
|
|
267
|
-
## PROGRESS CONTRACT
|
|
268
|
-
Emit `PLAN: <steps>` up front, then a single-line `PROGRESS: [<n>/<total>] <action>` before every
|
|
269
|
-
relevant action and `PROGRESS: [<n>/<total>] done` on completion, spaced so no gap exceeds ~2 min,
|
|
270
|
-
stdout unbuffered, and exactly one final `RESULT: DONE|BLOCKED <reason>`.
|
|
271
|
-
|
|
272
|
-
## FIX ROUND 1 (Orchestrator, post-review — read this if you are resuming this WO)
|
|
273
|
-
|
|
274
|
-
Your prior chunk (`RESULT: DONE`) is already in the working tree, uncommitted: the orchestrator
|
|
275
|
-
independently confirmed 221/221 tests pass (41 files, up from a 215/40 baseline) and `tsc` is clean. Do
|
|
276
|
-
NOT redo or re-verify what already works. The orchestrator also fixed one pre-existing test itself
|
|
277
|
-
(`tests/messagingUnreadLifecycle.test.jsx`'s "marks the selected conversation read" — it rendered
|
|
278
|
-
`ConversationList` alone with no `Thread`, which your scope D change legitimately broke since `Thread`'s
|
|
279
|
-
mount effect is now sole owner; already corrected and re-verified green, no action needed from you on
|
|
280
|
-
that file). Fix only the following, then re-run your test scope and report `RESULT: DONE` again.
|
|
281
|
-
|
|
282
|
-
**[P1, blocking — confirmed by the independent `reviewer`] Scope C's fix does not actually work for
|
|
283
|
-
direct (1:1) conversations — the single most common case.** `ReadTicks.jsx`'s merge of `cache.receipts`
|
|
284
|
-
over the REST-fetched `status` only takes effect through `recipientDetail` (derived from
|
|
285
|
-
`status.recipient_detail`). But per this repo's own established server contract
|
|
286
|
-
(`work-orders/MSG-3f.md:267`: `recipient_detail` is only present for non-direct conversations — dcm's
|
|
287
|
-
`read_status` never returns it for a `kind: 'direct'` conversation, by design, per the DM-privacy
|
|
288
|
-
carve-out). So for a DM: `applyFrame` correctly writes the incoming `read_state` frame into
|
|
289
|
-
`cache.receipts`, but `ReadTicks` never looks at `cache.receipts` for that case — `recipientDetail` stays
|
|
290
|
-
`undefined`, and `allRead` falls back to the stale `status.all_read` from the one-time REST fetch. The
|
|
291
|
-
defect this scope exists to fix ("a receipt can never update within a session", WO finding 7) is therefore
|
|
292
|
-
still present for every 1:1 conversation. Your own new test (`tests/messagingMsg6.test.jsx`, the
|
|
293
|
-
`read_state` test) only exercises `conversation: { kind: 'group' }` — write a DM-scoped equivalent that
|
|
294
|
-
would have caught this.
|
|
295
|
-
|
|
296
|
-
Fix: for a direct conversation, derive the receipt from `cache.receipts` keyed by the conversation's
|
|
297
|
-
counterpart, not from `recipient_detail` (which will never be populated for DMs — do not wait for it).
|
|
298
|
-
`conversation.other_user_id` (dcm's bare-id field for a direct conversation's counterpart, added in a
|
|
299
|
-
prior WO) is what you need: look up `cache.receipts[\`${conversation.id}:${conversation.other_user_id}\`]`
|
|
300
|
-
directly when `conversation?.kind === 'direct'`, and treat its `last_read_at` presence as `all_read` for
|
|
301
|
-
that case (a DM has exactly one other participant, so "all read" and "counterpart has read" are the same
|
|
302
|
-
fact — no `.every()` needed, unlike the group/managed case). Keep the existing `recipient_detail`-based
|
|
303
|
-
path for non-direct conversations unchanged. Add a test mirroring the existing group-conversation
|
|
304
|
-
`read_state` test but with `conversation: { id: 'c1', kind: 'direct', other_user_id: 'u2' }`, asserting the
|
|
305
|
-
label flips from `DELIVERED` to `ALL_READ` after the frame arrives, with no additional REST call.
|
|
306
|
-
|
|
307
|
-
Re-run your test scope (the messaging test files plus whatever covers `SupportRecoveryRequestsTab`) after
|
|
308
|
-
this change. Do not touch anything outside `ReadTicks.jsx` and the one new/extended test.
|
package/work-orders/MSG-6f.md
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# WORK ORDER MSG-6f (ui-core-micha) — the host title resolver is never called for broadcasts, and the read ticks claim a delivery that does not exist
|
|
2
|
-
|
|
3
|
-
**EXECUTION DIRECTIVE.** If you are the implementer reading this as your own spec, this section is not
|
|
4
|
-
addressed to you — it tells the Orchestrator how to invoke you; you ARE that invocation, do not shell
|
|
5
|
-
out to `codex exec`. Orchestrator: implement through `codex exec` in the background, invoked **directly
|
|
6
|
-
via Bash** (never the `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check`
|
|
7
|
-
and `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from this
|
|
8
|
-
file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
|
|
9
|
-
|
|
10
|
-
Feature-level WO (Expertenchat envelope). Both defects were found by **verifying the landed MSG-6e /
|
|
11
|
-
MSG-7 / MSG-8 / MSG-10 work in a running app** (jg-ferien local, flag on, ucm 2.21.3, dcm 2.39.5,
|
|
12
|
-
2026-08-03). Both are cases of "the other side shipped its half; ucm still renders the old behaviour".
|
|
13
|
-
|
|
14
|
-
## TIER
|
|
15
|
-
Tier 2 — shared-core, consumed by every app. Independent `reviewer` mandatory. `ui_reviewer` for
|
|
16
|
-
scope B (it changes what a user believes about message delivery).
|
|
17
|
-
|
|
18
|
-
## SCOPE
|
|
19
|
-
|
|
20
|
-
**A. `titleOf` never calls the host resolver for a broadcast — jg's fix is dead code.**
|
|
21
|
-
|
|
22
|
-
`ConversationList.jsx` `titleOf()` gates the host callback on kind:
|
|
23
|
-
|
|
24
|
-
```js
|
|
25
|
-
if (conversation.kind === 'managed' && resolveManagedLabel) {
|
|
26
|
-
const label = resolveManagedLabel(conversation);
|
|
27
|
-
if (label) return label;
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
jg-ferien MSG-10 correctly taught its `resolveManagedConversationLabel` to handle
|
|
32
|
-
`kind === 'broadcast'` — but ucm never calls it for that kind, so the branch is unreachable from the
|
|
33
|
-
list and the row still falls through to `t('MessagingList.UNTITLED')`.
|
|
34
|
-
|
|
35
|
-
**Verified live after MSG-10 landed:** the same conversation (`kind: "broadcast"`,
|
|
36
|
-
`external_key: "broadcast"`, `title: null`) renders **"Unbenannte Unterhaltung"** in the list and
|
|
37
|
-
**"Event-Ankündigungen"** in the open thread header — the thread header works only because jg's own
|
|
38
|
-
`resolveConversationTitle` calls the resolver directly, with no kind gate.
|
|
39
|
-
|
|
40
|
-
**The fix: stop gating on kind.** Call `resolveManagedLabel` for **every** conversation and let the
|
|
41
|
-
host decide — a `null` return already means "I have no label", which is exactly the delegation contract,
|
|
42
|
-
and the comment directly above `titleOf` already describes the label as host-supplied. Gating on kind
|
|
43
|
-
inside ucm duplicates a decision that belongs to the host and will break again for the next kind.
|
|
44
|
-
|
|
45
|
-
Check `resolveDirectUserName`'s gate (`kind === 'direct' && other_user_id != null`) in the same pass and
|
|
46
|
-
state whether it has the same problem. Do not change it blindly — a direct conversation is the one kind
|
|
47
|
-
where the id-based lookup is genuinely required.
|
|
48
|
-
|
|
49
|
-
**B. The read ticks assert a delivery that the platform does not track.**
|
|
50
|
-
|
|
51
|
-
Operator report, 2026-08-03: *"die Häkchen geben das Gefühl, dass jede Nachricht sofort zugestellt ist,
|
|
52
|
-
auch wenn das Browserfenster geschlossen ist."* Correct, and it is now strictly worse than when it was
|
|
53
|
-
written:
|
|
54
|
-
|
|
55
|
-
- dcm MSG-7 scope C **removed `delivered_count`** from the `read-status` response — verified live, the
|
|
56
|
-
response now carries only `all_read` and `recipient_detail`.
|
|
57
|
-
- `ReadTicks.jsx` still renders
|
|
58
|
-
`t('MessagingReadTicks.DELIVERED', { count: renderedStatus.delivered_count || 0 })`. With the field
|
|
59
|
-
gone, `undefined || 0` produces the **identical** label as before. Verified live: every own message
|
|
60
|
-
shows `aria-label="Zugestellt an 0"`.
|
|
61
|
-
|
|
62
|
-
So the UI states "Zugestellt an 0" — simultaneously claiming a delivery and reporting that it reached
|
|
63
|
-
nobody — on a single outlined check that reads as WhatsApp's delivered-tick. There is no delivery
|
|
64
|
-
signal behind it at all.
|
|
65
|
-
|
|
66
|
-
**Resolution (operator-directed, 2026-08-03): show a read PROPORTION in group and broadcast, keep a
|
|
67
|
-
two-state tick in DMs.**
|
|
68
|
-
|
|
69
|
-
The operational need in a camp is *"did a relevant share of the group read this?"* — a leader posting
|
|
70
|
-
"bus leaves at 14:00" to forty participants needs a number, not a checkmark. `all_read` is the wrong
|
|
71
|
-
measurement, not a useless feature: in any group it is essentially never true, so a boolean pins the
|
|
72
|
-
indicator to its first state precisely where the information matters most.
|
|
73
|
-
|
|
74
|
-
Operator decision, 2026-08-03:
|
|
75
|
-
|
|
76
|
-
| Conversation kind | Viewer | Display |
|
|
77
|
-
|---|---|---|
|
|
78
|
-
| `direct` | sender | two-state tick — `DoneOutlinedIcon` **light grey** "Gesendet" → `DoneAllOutlinedIcon` **dark blue** "Gelesen" |
|
|
79
|
-
| `group`, `broadcast`, `managed` | team member | a **ratio**: "18/40" (read / members). Numeric, no icon ladder. |
|
|
80
|
-
| `group`, `broadcast`, `managed` | ordinary participant | **nothing** — render no indicator at all |
|
|
81
|
-
| per-recipient detail | team member | unchanged — already correctly gated (see below) |
|
|
82
|
-
|
|
83
|
-
**The contrast between the two DM states must be clearly stronger than today.** Currently
|
|
84
|
-
`color={status.all_read ? 'primary' : 'inherit'}`, and `inherit` inside the meta `Stack` resolves to
|
|
85
|
-
`text.disabled` — the two states are barely distinguishable. Use an explicit light grey and an explicit
|
|
86
|
-
dark blue rather than `inherit`/`primary`, and verify the difference is obvious at the rendered
|
|
87
|
-
`1rem` icon size in both light and dark theme. The icon shape already differs (`Done` vs `DoneAll`), so
|
|
88
|
-
the distinction does not rest on colour alone — keep it that way.
|
|
89
|
-
|
|
90
|
-
**"nothing" for an ordinary participant in a group is deliberate, not an oversight.** A permanently
|
|
91
|
-
grey tick — which is what `all_read` would produce in any real group — is the "looks stuck" failure this
|
|
92
|
-
scope exists to remove. No indicator is better than a misleading one.
|
|
93
|
-
|
|
94
|
-
**No client-side permission logic.** dcm MSG-9 returns the counts only to viewers holding
|
|
95
|
-
`read_receipt_detail`, so the rule is purely shape-driven: *counts present → ratio; else `direct` →
|
|
96
|
-
tick; else → render nothing*. Do not branch on roles or capabilities in ucm.
|
|
97
|
-
|
|
98
|
-
**The server already has the number and throws it away.** `read_status` (`services.py:344-347`) computes
|
|
99
|
-
`all_read` from exactly the queryset that yields the count:
|
|
100
|
-
`participants.filter(last_read_at__gte=message.created_at).count()` — the same shape as the
|
|
101
|
-
`delivered_count` that had no writer. **A `read_count` + `recipient_count` pair is the counter
|
|
102
|
-
`delivered_count` should have been.**
|
|
103
|
-
|
|
104
|
-
Adding them is a `django-core-micha` change, not ucm's: **this scope is blocked on `django-core-micha`
|
|
105
|
-
MSG-9**, which adds `read_count` and `recipient_count` inside the existing `read_receipt_detail`-gated
|
|
106
|
-
block. Keep `all_read` — the DM branch still uses it.
|
|
107
|
-
|
|
108
|
-
Do not derive the ratio client-side by counting `recipient_detail` yourself. It would happen to work
|
|
109
|
-
(both are gated on the same right), but it re-implements a server aggregate in the client and would
|
|
110
|
-
silently produce a wrong denominator the moment dcm's participant filtering changes. Read the counts
|
|
111
|
-
dcm sends, or render nothing.
|
|
112
|
-
|
|
113
|
-
Retire `MessagingReadTicks.DELIVERED` and its `{{count}}` regardless — the new ratio string is a
|
|
114
|
-
different key with different semantics, and reusing "Zugestellt" would carry the false claim forward.
|
|
115
|
-
Also stop reading the now-always-`null` `last_delivered_at` out of `recipient_detail`.
|
|
116
|
-
|
|
117
|
-
**Per-recipient detail stays as it is.** `services.py:340` already gates it on
|
|
118
|
-
`kind != DIRECT and "read_receipt_detail" in rights` (in jg: `MANAGER_RIGHTS`), and DMs never expose it
|
|
119
|
-
even to moderators — a documented design invariant. This WO does not touch that; the popover keeps
|
|
120
|
-
working for managers.
|
|
121
|
-
|
|
122
|
-
**Why not implement delivery tracking as well** (wiring `mark_delivered` from a client ack, technically
|
|
123
|
-
possible since dcm MSG-8 made WS frames arrive): in a browser app "delivered" can only mean "a tab had
|
|
124
|
-
the socket open at that moment". For a recipient who is simply not online it would sit at the first
|
|
125
|
-
state indefinitely — replacing "looks always delivered" with "looks permanently stuck", plus a write per
|
|
126
|
-
received message. It only becomes meaningful with guaranteed device-level push. A read ratio answers the
|
|
127
|
-
real question without that ambiguity. **If the operator wants delivery too, it is a feature WO.**
|
|
128
|
-
|
|
129
|
-
**C. The read signal misses the most attentive reader — fix this or the ratio understates.**
|
|
130
|
-
|
|
131
|
-
What `read_count` actually measures must be stated plainly, because it is weaker than "read":
|
|
132
|
-
`mark_read` (`dcm services.py:288-294`) sets `ConversationParticipant.last_read_at` to *now*, and after
|
|
133
|
-
MSG-6e scope D there is exactly **one** caller — `Thread.jsx:58`, a `useEffect` keyed on
|
|
134
|
-
`[conversationId, markConversationRead]`. So the recorded fact is **"opened this conversation"**, and
|
|
135
|
-
`read_count` counts participants who opened it at some point after the message was posted.
|
|
136
|
-
|
|
137
|
-
It is not tab-open, not socket-connected, not per-message interaction, not viewport visibility.
|
|
138
|
-
|
|
139
|
-
Two asymmetries follow. The first is acceptable, the second is a defect:
|
|
140
|
-
|
|
141
|
-
1. Opening a conversation marks its **entire** history read in one stroke. Someone who opens it and
|
|
142
|
-
leaves immediately counts as having read a 200-message backlog. This is the universal convention
|
|
143
|
-
(WhatsApp behaves the same) — accept it, do not try to fix it here.
|
|
144
|
-
2. **A participant already sitting in the open conversation when the message arrives is NOT counted.**
|
|
145
|
-
The effect is keyed on `conversationId`, so it does not re-fire when a new message lands. The person
|
|
146
|
-
who saw it in real time is recorded as not having read it, until they navigate away and back.
|
|
147
|
-
|
|
148
|
-
Asymmetry 2 is newly common and newly consequential: before dcm MSG-8 no frame was ever delivered, so
|
|
149
|
-
no thread ever updated live and this case did not arise. Now it is the *normal* case for anyone
|
|
150
|
-
actually paying attention — and it biases the ratio **downward**, which is the wrong direction for a
|
|
151
|
-
leader deciding whether to chase people about the bus time.
|
|
152
|
-
|
|
153
|
-
**Fix: re-mark read when a new message arrives in the conversation currently open.** Trigger on the
|
|
154
|
-
incoming `message` frame for the active conversation, not on a timer. Guard it: do not fire for the
|
|
155
|
-
viewer's **own** send (dcm already excludes the sender from the fan-out, so no frame arrives — verify
|
|
156
|
-
rather than assume), and do not fire when the thread is mounted but the document is hidden
|
|
157
|
-
(`document.visibilityState`), or a background tab would report reads nobody performed. `mark_read` is
|
|
158
|
-
already idempotent — it only writes when the new timestamp is later (`services.py:291`) — so a
|
|
159
|
-
redundant call is cheap, but a *wrong* one is not.
|
|
160
|
-
|
|
161
|
-
**Known cost, do not silently ignore:** `Thread` mounts `ReadTicks` per own message and each instance
|
|
162
|
-
fires its own `read-status` request — O(n) for a sender with many messages in a thread, which is exactly
|
|
163
|
-
the broadcast-heavy leader this feature serves. A batch endpoint (message ids → counts) is the natural
|
|
164
|
-
answer and belongs in the same dcm WO. Do **not** fold the counts into `serialize_message` instead:
|
|
165
|
-
`views.py:50-54` documents why viewer-specific read state must stay out of it — it would leak into the
|
|
166
|
-
`message`/`message_edited` realtime frames, the same trap `thread_last_read_at` was kept out of.
|
|
167
|
-
|
|
168
|
-
## NON-GOALS / DO NOT TOUCH
|
|
169
|
-
- Do not reintroduce `delivered_count` on either side.
|
|
170
|
-
- Do not change `all_read`'s semantics or how `read_state` frames update the cached receipt (that is
|
|
171
|
-
MSG-6e scope C, already landed — leave it working).
|
|
172
|
-
- No change to `isOwn`/`senderName`/`canShowReadTicks`; the `sender` object from dcm 2.39.4 fixed those
|
|
173
|
-
and they are **verified working** (all server-loaded messages now render `own` with a tick).
|
|
174
|
-
- Do not touch jg-ferien. Its `resolveManagedConversationLabel` is already correct and must stay as it
|
|
175
|
-
is — scope A is what makes it reachable.
|
|
176
|
-
|
|
177
|
-
## RISKS
|
|
178
|
-
- **A changes behaviour for every host app**, including any that relies on `resolveManagedLabel` being
|
|
179
|
-
called only for `managed`. A host returning a non-null label for an unexpected kind would newly see
|
|
180
|
-
it win over `conversation.title`. Check the ordering in `titleOf` after the change: a host label
|
|
181
|
-
should still lose to nothing it previously lost to, apart from the intended new case.
|
|
182
|
-
- B changes a user-facing claim. Getting it wrong in the other direction (removing the indicator
|
|
183
|
-
entirely) loses the "did my message go out" affordance — hence two states, not zero.
|
|
184
|
-
- The retired i18n key may be referenced by host apps' tests. Grep before deleting and note any hits.
|
|
185
|
-
|
|
186
|
-
## REQUIRED TESTS TO WRITE
|
|
187
|
-
Narrow and behavioural. Do NOT run the full suite.
|
|
188
|
-
|
|
189
|
-
1. **A:** a conversation with `kind: 'broadcast'`, `title: null`, and a host `resolveManagedLabel` that
|
|
190
|
-
returns a string renders **that string**, not `MessagingList.UNTITLED`. This must **fail against
|
|
191
|
-
today's code** — prove it.
|
|
192
|
-
2. **A:** a host resolver returning `null` still falls through to the existing chain unchanged
|
|
193
|
-
(regression guard for the delegation contract).
|
|
194
|
-
3. **A:** the existing `managed` / `event_all` / `event_team` behaviour is unchanged.
|
|
195
|
-
4. **B:** `direct` renders the two-state tick and the two states use visibly different explicit colours (assert the resolved colours differ, not just that a colour prop is set); a `group` WITH counts renders the ratio; a `group` WITHOUT counts renders **no** indicator. Assert no label containing "Zugestellt" is produced anywhere in `ReadTicks`.
|
|
196
|
-
that **no** label containing a `{{count}}`-derived number is produced anywhere in `ReadTicks`.
|
|
197
|
-
5. **B:** `all_read` still renders the double check in `primary` with the unchanged label.
|
|
198
|
-
|
|
199
|
-
**Non-vacuity:** tests 1 and 4 must fail on the current code. Test 1 is the one that matters — jg
|
|
200
|
-
shipped a fix for this exact symptom that did nothing, because the test asserted on the wrong side of
|
|
201
|
-
the boundary. Assert on the **rendered list row**, not on the resolver function in isolation.
|
|
202
|
-
|
|
203
|
-
## TEST SCOPE FOR THE GATE (orchestrator)
|
|
204
|
-
`ConversationList` and `ReadTicks` test files plus the messaging i18n tests. Not the full suite.
|
|
205
|
-
|
|
206
|
-
## TARGET REPO
|
|
207
|
-
`C:\Users\biglmi\Documents\webapps\ui-core-micha`. Branch `main` (or `develop` if present). Publish per
|
|
208
|
-
the repo's release flow. jg's pin bump is a separate step; jg needs it to close its own MSG-9
|
|
209
|
-
finding 4/12, which cannot be fixed from jg at all.
|
|
210
|
-
|
|
211
|
-
## PROGRESS CONTRACT
|
|
212
|
-
Emit `PLAN: <steps>` up front, then a single-line `PROGRESS: [<n>/<total>] <action>` before every
|
|
213
|
-
relevant action and `PROGRESS: [<n>/<total>] done` on completion, spaced so no gap exceeds ~2 min,
|
|
214
|
-
stdout unbuffered, and exactly one final `RESULT: DONE|BLOCKED <reason>`.
|