Plan-First Clock Flow: Gates, Drafts, RichEditor Composer, and SuperChat Feed (Milestones 1–7) - #446
Conversation
Milestone 1 of the plan-first clock flow: teams.updateSettings method, settings on the public team shape, teamApi.updateSettings, and a Switch in the Team Settings modal (admins only, default off).
…yPost hook Milestone 2 of the plan-first clock flow: posts can carry a client-local YYYY-MM-DD postDate, huddle.updatePost stamps wrapUpAt when wrapUp is passed, and huddle.getMyPostForDate + useDailyPost expose today's post.
Milestone 3 of the plan-first clock flow:
- clock.stop throws Meteor.Error('plan-required') when the team setting
is on and today's post has no wrap-up; accepts an optional client-local
localDate so 'today' matches the user's timezone
- ClockPage disables Clock In without today's post and Clock Out without
a wrap-up, with inline messages linking to Huddle
- useClockToggle surfaces plan-required refusals inline instead of alert
- wormhole exposure for teams.updateSettings + huddle.getMyPostForDate
- integration tests: meteor-backend/tests/plan-gate.test.ts (9 passing)
🗑️ Preview Environment Cleaned UpThe preview container for this PR has been deleted. |
The clock-page gate now embeds the same HuddleComposer used on the Huddle tab (PlanComposer wrapper): plan mode creates today's post, wrap-up mode appends a wrap-up and stamps wrapUpAt — no navigation needed. Gates now target the active session's team, not the selected team, so clock-out feedback shows even after switching teams. Extracted shared avatar helpers and toPostAttachment to remove duplication.
…page - useDailyPost now subscribes to the huddlePosts.byTeam publication, so the clock-in/clock-out gates flip in realtime on post create/edit/ delete — no reload and no manual refetch events needed - the mobile bottom-nav clock FAB navigates to the clock page instead of toggling directly, so the gates and inline composer are always visible - dropped the now-unneeded huddle:refetch window events
Centralize the gate in useClockToggle (planGate: planMissing/ wrapUpMissing/todayPost, realtime via useDailyPost) so all clock entry points agree: - clockIn() refuses while today's plan is missing and reports success - bottom-nav FAB shows a dimmed 'plan required'/'wrap-up required' state on any page (dashboard included) while still opening the clock page where the inline composer lives - Work/Tickets clock-in prompts surface the gate instead of silently clocking in without a plan - ClockPage now consumes the shared planGate instead of computing its own
… remove PlanComposer
… shift Milestone 4 (added by request): posts accept draft: true (author-only, date-less, status: 'draft'); the feed, publication/change stream, and both clock gates only see published posts. huddle.publishPost stamps the client-local postDate + clears status (arriving in the feed as a realtime added), huddle.getMyLatestDraft fetches the newest draft. Clock page composer gets Save/Update draft and 'Publish plan and clock in'. Plan doc updated: drafts promoted from out-of-scope to Milestone 4, milestones renumbered, design-iteration notes recorded.
…0.6.1 (file:vendor/ui) Dependency prerequisite for the RichEditor/SuperChat milestones. The library is built locally from the vendor/ui submodule; postinstall runs scripts/ensure-ui-build.mjs so a fresh clone self-heals (no-op when dist/ exists, SKIP_UI_BUILD=1 to opt out), plus a manual setup:ui script. Smoke-tested: typecheck, lint, 76 unit tests, production build, and runtime render all clean on 0.6.1.
Milestone 5: the Huddle composer now uses RichEditor from @mieweb/ui/kerebron (WYSIWYG ProseMirror, markdown in/out) — the manual markdown toolbar and preview tabs are gone. @kerebron/* peer deps installed; a small Vite plugin serves @kerebron/wasm assets at /kerebron-wasm in dev and copies them into dist on build; kerebron.css imported in styles.css. When today's post exists the composer opens it for editing (remounted via key) and submit updates instead of creating. Mentions become removable chips below the editor.
Milestone 6: huddle posts map onto a SuperChatConversation (one participant per author, one message per post; image attachments embedded as markdown, others as links, ticket title as an inline tag) rendered with <SuperChat order='desc' readOnly virtualized> and code/image/ mermaid render plugins. Self-authored messages are editable inline via onMessageEdited → huddle.updatePost. A chat/cards toggle keeps the classic card view for per-post comments and likes (SuperChat has no per-message threads — deliberately not force-fit).
Three product changes by request: 1. Per-session gate (was once-per-day): posts carry clockEventId; clock.start links the plan post to the new session (planPostId), clock.stop gates on that session's post. Every clock-in needs a fresh plan; a 2nd clock-in the same day needs its own. New huddle.getMyPostForSession + useSessionPost hook; useClockToggle exposes sessionPost. Recovery path (createPost accepts clockEventId + wrapUp) so a session can never get stuck. Huddle-tab composer now always creates a new post. 2. Drafts tab: Feed/Drafts toggle on the Huddle page + DraftsPanel for creating/editing/publishing/deleting multiple drafts. New huddle.getMyDrafts; publishPost accepts clockEventId. 3. Bigger editor: RichEditor min-h-52 + larger text; clock-page textarea rows=10/min-h-52. Team Settings copy updated to per-session. Tests updated (15 backend integration, 76 unit). In-browser verified: two independent sessions, two feed posts, drafts created and hidden from the feed.
…k-outside collapse - Shared MarkdownEditor wrapper: a capturing mousedown handler preventDefaults clicks on the Kerebron toolbar/menu (.kb-custom-menu__wrapper) so the editor keeps its selection — toolbar buttons (Toggle bold/italic/etc.) now apply to the selected text instead of no-oping. Also handles Cmd/Ctrl+Enter submit. - Clock page now uses the same Kerebron RichEditor (was a plain textarea); remounts via key to clear after posting and re-seed when a draft loads. - Huddle composer collapses on click-outside when empty, freeing up feed space; typed content is preserved (never collapses with content). Verified in-browser: toolbar bold via mouse, clock-page plan/wrap-up flow through the rich editor, and click-outside collapse.
…inuity; update MarkdownEditor for toolbar interaction; add dark mode styles for Kerebron RichEditor
…endabot - Bump @kerebron/extension-basic-editor ^0.7.9 → ^0.8.6 so it no longer pulls a second @kerebron/editor / prosemirror-model copy (duplicate ProseMirror instance warnings). Deduped the lockfile. - Add .github/dependabot.yml: group @kerebron/* into one weekly PR (release train) and bump the vendor/ui + vendor/meteor-wormhole submodule pointers.
…mposer edit - Remove the camera button from the collapsed composer bar. - Add a Pulse video button with full ticket-details parity: QR record with phone + device upload, via a shared PulseUploadModal (extracted so the ticket-scoped PulseUploadButton and the new library-mode PulseAttachButton share one modal). Library uploads reserve with no ticket and poll mediaApi.list by videoid for completion. - Mentions now render: they were stored in content.mentions but never inserted into the post text (posts showed nothing). Append @name into the text on submit. - Edit a post via the full HuddleComposer (RichEditor + Photo/Video/Doc/ Pulse/Ticket/@Mention) instead of a plain textarea, so attachments, tickets and mentions are all editable. Backend huddle.updatePost now accepts + persists attachments and ticketId (omitted → untouched, so the plan-first clock flow is unaffected). - Fix z-index: Kerebron's sticky toolbar (z-index:1000) bled over @mieweb/ui modals (z-50); pull it into the app z-scale (z-index:10). - Update clock-post-simple-plan.md (Milestone 9) to match. Verified in-browser: camera gone; Pulse modal (QR + device); mentions render in the feed and persist in the DB; edit preloads content and saves with the 'edited' badge; clean modal stacking. typecheck / eslint / prettier clean.
Add a minimal y-websocket-compatible relay attached to Meteor's HTTP server on a dedicated /yjs path (clear of DDP's /websocket). One in-memory Y.Doc per huddle post room; no persistence — markdown on the post stays the source of truth and the first client seeds the room from it. Token-in-query auth via resolveToken; the doc is dropped once the last peer leaves. Moves ws to runtime deps and adds yjs, y-protocols, lib0.
Wire the RichEditor collab prop through the huddle composer: editing an
existing post now joins a /yjs room (room = post id) so two people editing
the same post co-edit one shared document. New-post composing stays
single-user. Auth reuses the Meteor resume token; the WS URL targets the
Meteor backend, not the Vite origin.
- src/features/huddle/collab.ts: builds the CollabConfig (token + ws url)
- MarkdownEditor / HuddleComposer: thread a collab / collabRoom prop
- PostCard: enable collab when editing (collabRoom={post.id})
- bump vendor/ui submodule to the feat/richeditor-collab-yjs branch commit
- add @kerebron/extension-yjs, yjs, y-protocols to root deps
- update the plan: M8 relay + wiring done, two-browser verify pending
An earlier `npm install` while adding the Yjs deps reconciled @mieweb/pulsevault to the broken registry stub (`^0.1.0`, empty dist), crash-looping the server with 'Cannot find module ./dist/core.js'. The working lockfile always resolved it from github (commit f41f5579), so point package.json at the github source to match — stable under future installs. Also marks M8 verify done: the /yjs relay is live (startup log, 401 for unauthenticated) and two authenticated peers synced an edit end-to-end.
@mieweb/ui is linked via file:vendor/ui, whose node_modules can carry a second copy of react/react-dom after a submodule `npm install`. Two React instances break hooks — the Huddle feed crashed with 'Invalid hook call' / 'Cannot read properties of null (reading useReducer)' in @mieweb/ui's VirtualThread. resolve.dedupe pins a single React.
Two-window Yjs co-editing was crashing/reloading a window (which reset the selected team to Personal) and made the feed feel like it auto-refreshed while editing. Stop wiring collabRoom into the inline feed composer so post editing is stable single-user again. The /yjs relay and RichEditor `collab` prop stay in place (dormant) for a future, better-isolated surface.
Fix: PostCard was opening the edit HuddleComposer without passing
collabRoom — so Yjs never connected and edits were local-only. Added
collabRoom={post.id} to the edit-mode render.
Also added data-testid='post-card' to the PostCard root div for test
targeting.
Vendor (vendor/ui): filter 'autocomplete' and 'hover' extensions from
CollabAdvancedEditorKit — both crash with null.matchesNode() in
EditorView.updateStateInner when a Yjs remote update arrives, leaving
the editor in a permanently broken state.
E2E (tests/e2e/huddle/yjs-collab-editing.spec.ts):
- Two isolated browser contexts (member1 + admin1)
- member creates a post, both open edit composer in Cards view
- admin types -> member sees it live (via toPass retry loop, ≤8s)
- member types back -> admin sees it live
- Second test verifies a /yjs/<postId> WebSocket is established
Frontend/iOS checks: add submodules: true to actions/checkout@v4 so ensure-ui-build.mjs postinstall can find vendor/ui/package.json and build the library before npm ci completes. Dockerfile (PR preview): vendor/ui needs to be built before the main npm install because package.json declares '@mieweb/ui': 'file:vendor/ui'. Copy vendor/ui first, run npm ci + build inside it, then install the app with SKIP_UI_BUILD=1 (dist already present). Also exclude vendor/ui/node_modules from the Docker build context via .dockerignore to avoid sending unnecessary files to the daemon.
The vendor/ui submodule is on a local branch (feat/richeditor-collab-yjs) that has not been pushed to the public mieweb/ui remote. Any workflow step that tries to init the submodule fails with a 'reference not found' error. checks.yml (frontend + iOS): - Remove submodules: true (would fail at checkout) - Swap @mieweb/ui to 0.6.1-dev.169 (published) via npm pkg set before npm install; SKIP_UI_BUILD=1 bypasses the postinstall that would try to build the missing submodule pr-preview.yml: - Change submodules: true -> false; explicitly init only vendor/meteor-wormhole (which IS in its remote) so the Meteor build can find its packages Dockerfile: - Remove COPY vendor/ui (revert previous attempt); instead sed-replace the file: dep with 0.6.1-dev.169 and run SKIP_UI_BUILD=1 npm install src/vendor-augments.d.ts (new): - TypeScript module augmentation that adds CollabConfig + RichEditor.collab to @mieweb/ui/kerebron via interface merging. CI installs the published package (which lacks these types); local dev uses file:vendor/ui which has them natively. Both environments typecheck cleanly.
Root cause of all three failing checks: the vendor/ui submodule points at a commit on a local branch (feat/richeditor-collab-yjs) that was never pushed to the public mieweb/ui repo. Every CI job that touched the submodule failed: - Frontend/iOS: submodule checkout / postinstall build failed - PR Preview: Docker submodule init failed The published @mieweb/ui (0.6.1-dev.169) is NOT a drop-in — its build errors on missing @mieweb/datavis exports our vendored build resolves. Fix — vendor the built library as a committed tarball: - vendor/mieweb-ui.tgz (3.5 MB, npm pack of vendor/ui) committed to the repo - package.json: '@mieweb/ui': 'file:vendor/mieweb-ui.tgz' (was file:vendor/ui) - Add @mieweb/datavis + datavis-ace as devDeps so the tarball's optional peer imports resolve at build time (they tree-shake out of the browser bundle) - Drop the ensure-ui-build postinstall (no submodule build needed); setup:ui now rebuilds + repacks the tarball - checks.yml / pr-preview.yml: back to plain npm ci, no submodule init for ui (pr-preview still inits vendor/meteor-wormhole for the Meteor build) - Dockerfile: COPY the tarball before npm install; drop the pkg-swap hack - Remove src/vendor-augments.d.ts — the tarball ships the real collab types Also fixes the Format check: prettier --write on 8 previously-unformatted files (ClockPage, DraftsPanel, MarkdownEditor, PostCard, useClockToggle, Huddle, yjs-collab-editing spec, clock-post-simple-plan). Verified locally: npm ci + typecheck + lint + format + build all clean.
The production stage runs 'npm install --production' against the root package.json, which references '@mieweb/ui': 'file:vendor/mieweb-ui.tgz'. The tarball was only copied in the builder stage, so the production install failed to resolve the file: dependency (exit 254). Copy it into the production stage before the install as well.
Teardown only deletes what it knows how to find — seed users by `@test.local`, teams by name, auxiliary rows by `userId` — so anything keyed differently (channels by team, tickets by creator, activities at all, signups like `onboard<ts>@example.com`) survived and accumulated run over run. Once the database is big enough the app slows down and time-boxed waits start failing in ways that look like product bugs. Add a global-setup reset that clears every non-system collection, so each run starts from a known state regardless of what earlier runs left behind. Uses deleteMany rather than drop to keep the indexes and validators the backend builds at startup and won't rebuild without a restart. SKIP_DB_RESET=1 opts out, mirroring teardown's SKIP_CLEANUP. Both hooks are destructive and run against whatever MONGO_URL resolves to, which shares a mongod with the dev database and is overridable — so guard on the database *name* and refuse anything not ending in `_test` rather than trusting the caller's intent.
…lectors Swapping the composer's `<textarea placeholder="What's on your mind?…">` for Kerebron's RichEditor dropped the prompt entirely — RichEditorProps has no `placeholder`, so the empty composer rendered with no hint at all. Reinstate it from the wrapper: MarkdownEditor takes a `placeholder` prop and exposes `data-empty` + `--markdown-editor-placeholder`, which styles.css renders as a `::before` floated into the editor's first line. Floating with `height: 0` keeps it out of layout flow, so the caret still sits at the line start and no hard-coded offset is needed to clear the toolbar. The same swap broke two specs that reached for the old textarea: `getByPlaceholder(/What's on your mind/i)` no longer matches a contenteditable, and in the realtime spec a loose `textarea` selector silently matched the unrelated, disabled "Read-only conversation" composer. Both now target `.markdown-editor .ProseMirror` and expand the collapsed composer first. Also switch the video specs to card view before asserting on inline <video>: the feed defaults to chat view, where non-image attachments render as plain links by design (superChatFeed.ts), so the assertion could never pass there.
The deep-link effect matches `teamId` against the loaded team list but ran before that list arrived, and it consumes the query string destructively via replaceState. So on an early run the team never matched, the params were stripped anyway, and every later run saw an empty search — the link was lost for good and the user landed on whichever team sorts first. Gate the effect on `teamsReady` so it runs once the data it needs is actually there. The admin half of the timesheet spec papered over this by deep-linking and hoping: it now selects the team and member explicitly, since switching teams resets the panel's member selection to whoever sorts first and discards the link's `memberId` regardless.
…reconnect Foreground retries give up after ~3.5s, far short of a backend restart, so a tab left open across one stayed dead until a manual reload — and the session fetch that failed meanwhile left it looking logged out. Add a self-rescheduling background retry with capped exponential backoff (1s → 30s) that runs until the server returns, plus an `onReconnect` hook for callers that gave up while it was down. SessionProvider subscribes and refetches, so the session restores itself without a reload.
|
@SarkarShubhdeep @jlocala1 to review |
The cross-session sync tests in realtime/huddle-posts.spec.ts and huddle/yjs-collab-editing.spec.ts were comparing two disjoint feeds: each logged-in user defaults to their private Personal team, so posts made by one user were never visible to the other and the tests failed against a correctly-behaving app. Add tests/e2e/fixtures/team.ts with selectSharedTestTeam(), which looks up the seed team TEST01 (Test Team Alpha — provisioned by global-setup with every @test.local user as a member) and switches the page onto it via localStorage + reload. Call it from both specs' beforeEach. Fixes 4 previously-failing tests; full suite now 133/133.
- Add plan-first-clock-flow.spec.ts with comprehensive test scenarios covering plan requirement enabled/disabled, clock in/out with plan and wrap-up, draft plans, and huddle feed verification - Add HuddlePage and TeamSettingsPage page objects - Extend ClockPage with plan gate and wrap-up interactions - Update TeamContext to support plan-first clock flow - Update vendor/ui submodule
4baf7f4 to
f0c12c8
Compare
There was a problem hiding this comment.
It's a big feature and the gate design is well thought through. The backend clock-out enforcement being server-side and actually tested is exactly right, and there are some good race-condition fixes tucked in here too (the TeamContext readiness guards, the ddp reconnect refetch, the teamsReady guard before the deep-link replaceState).
That said, I think a handful of things need to land before this can merge. Sticking to the ones that actually matter.
Blocking
1. Vendored @mieweb/ui isn't reproducible and ties main to an unmerged fork PR.
The vendor/ui submodule is pinned to a SHA that only exists on a personal fork (Dharp02/ui). On mieweb/ui it's reachable only as refs/pull/344/head, which git submodule update --init won't fetch, so nobody can init the submodule, diff the source, or rebuild the committed 3.6MB vendor/mieweb-ui.tgz to confirm it matches. mieweb/ui#344 is also open and currently conflicting, so main would be riding a still-changing branch through an opaque, unpublished binary that ships its own preinstall script.
A few knock-on problems: the postinstall the description mentions doesn't actually exist in package.json, so scripts/ensure-ui-build.mjs is orphaned dead code (and it still points at file:vendor/ui while the real dep is file:vendor/mieweb-ui.tgz); npm run setup:ui is broken for the same fetch reason; and the gitsubmodule dependabot entry will try to bump the pin off the feature branch.
Cleanest path is to cut a real prerelease of @mieweb/ui and depend on that, gated on mieweb/ui#344 landing, rather than vendoring a blob. If a tarball has to ship short-term, at minimum point the submodule at the fork and SHA that actually built it, give setup:ui a fetch that works, delete the orphaned script, and drop the submodule dependabot updater.
2. The Yjs relay authenticates the user but not the room, and the client puts the account token in the URL.
yjs.js checks identity but does no per-post authorization, so any logged-in user can open /yjs/<anyPostId> and read or inject edits into any post's live doc across teams (the code comment itself flags this as a follow-up). collab.ts sends the meteor_resume_token, which is the long-lived account credential, in the WebSocket URL query string, so it ends up in proxy and access logs, browser history, and Referer headers. One logged handshake is enough for account takeover. docName is also an unvalidated string, so it doubles as unbounded in-memory room creation.
Team membership (ideally author) should be checked before handleUpgrade, the token should move out of the URL into a subprotocol or first message, and docName should be validated as a real postId. There are also no negative tests on any of this, so nothing proves it isn't an open relay.
3. Mermaid in the feed bypasses sanitization.
The feed now renders every teammate's post through the SuperChat mermaid plugin, which does dangerouslySetInnerHTML on the SVG and skips rehype-sanitize, unlike the app's other mermaid renderer (MermaidBlock.tsx) that runs the SVG through DOMPurify. With mermaid's securityLevel: 'strict' as the only guard, and mermaid's history of parser bypasses, a crafted mermaid block is stored XSS for everyone viewing the team feed. The fix lives on the @mieweb/ui side: give the SuperChat mermaid plugin the same DOMPurify pass that MermaidBlock uses, or drop mermaid from the feed.
4. Clock-in has no server enforcement.
clock.stop enforces requirePlanForClock, but clock.start only links the plan post, it never checks one exists (there's even a passing test showing clock.start succeeds with the gate on and no plan). On the client the gate also disappears before team settings load, and clockIn has no catch to surface a server refusal. So the clock-in half of the gate is client-only and trivially bypassable with a direct method call. Either enforce it in clock.start to match clock-out, or make it explicit that clock-in is only a UX nudge.
Also worth fixing before merge
Duplicate wrap-up on a realtime race (ClockPage.postWrapUpAndClockOut + useSessionPost). The recovery branch fires whenever sessionPost is falsy, which includes the whole window while huddlePosts.byTeam is still syncing after a refresh. So refresh while clocked in, type a wrap-up, submit before sync resolves, and you get a second post created while the real plan never gets its wrap-up. Gate the button on the subscription being ready.
Editor seed vs text desync (ClockPage). The editor is fed value={seedText} but posting uses a separate text state, and editorKey bumps unconditionally on a late seed, so the editor can remount showing the plan text while text still holds what the user typed (and submit saves content they can no longer see). Drive the editor from text, or only bump the key when the seed actually replaced text.
Turning the setting on mid-shift strands open sessions (clock.js). A session with no linked plan post can never clock out, since post is null so plan-required always throws. The createPost recovery path exists but needs to be surfaced for sessions that are already open when the setting flips on.
Test gaps on the parts that matter most. Cross-user draft read isolation is coded but never tested, the client-local-date midnight boundary is never exercised (every test runs inside a single local day), and a couple of the e2e checks are tautological (if (x.includes(y)) expect(x).toContain(y)), so they pass whether or not the behavior works. The backend clock-out and admin-only tests are genuinely strong, these are the holes.
Happy to pair on any of these. The gate and drafts backend is in good shape, it's mainly the Yjs milestone and the ui vendoring I'd want reworked before this goes in.
Blocking Issues ProgressThanks for the review! Here's the status of the 4 blocking items: ✅ 1. Yjs relay authenticates the user but not the roomStatus: FIXED in this PR In , we implemented comprehensive per-room authorization:
❌ 2. Vendored
|
|
LGTM! |
SarkarShubhdeep
left a comment
There was a problem hiding this comment.
Files are well commented. Everything looks fine.
Overview
The plan-first clock flow (clock-post-simple-plan.md): write today's plan as a Huddle post → clock in → add a wrap-up to that same post → clock out. Gated per team by one setting, off by default — with the setting off, everything behaves exactly as today.
Now covers Milestones 1–7 plus the vendor/ui dependency prerequisite and Milestone 8 (Yjs live-sync).
Milestone 1 — Team setting
settings.requirePlanForClockon the team doc (absent =false), admin-onlyteams.updateSettings,Switchin the Team Settings modalMilestone 2 — Today's post
postDate(client-localYYYY-MM-DD) on posts,wrapUp→wrapUpAt,huddle.getMyPostForDate, realtimeuseDailyPosthookMilestone 3 — Gates (+ design iteration)
clock.stopthrowsplan-requireduntil it has a wrap-up (client-locallocalDatefor timezone correctness)useClockToggle.planGate, realtime via thehuddlePosts.byTeamDDP publication — every surface agrees: clock page, bottom-nav FAB (dimmed "plan required" state, navigates to the clock page), Work/Tickets clock-in promptsMilestone 4 — Drafts
draft: trueposts: author-only, date-less,status: 'draft'; excluded from feed, publication/change stream, and both gateshuddle.publishPoststampspostDate+ clears status (arrives in the feed as a realtimeadded);huddle.getMyLatestDraftDependency prerequisite — vendor/ui submodule + prebuilt tarball
mieweb/uivendored as a git submodule atvendor/uipackage.jsoninstalls from a committed prebuilt tarball:"@mieweb/ui": "file:vendor/mieweb-ui.tgz"(~3.6 MB, in the diff of this PR)npm run setup:uirebuilds the submodule and regenerates the tarball;postinstallrunsscripts/ensure-ui-build.mjsso fresh clones self-heal (no-op when built,SKIP_UI_BUILD=1to skip)Why a committed tarball and not npm?
@mieweb/uiis on the public npm registry (latest0.7.0), but no published version contains the Yjs collaborative-editing feature that Milestone 8 depends on. That work lives on thefeat/richeditor-collab-yjsbranch ofmieweb/ui, tracked by mieweb/ui#344 — Add Yjs Collaborative Editing to RichEditor and Fix Editor Teardown Crash. Three unreleased commits:83863e1—feat(RichEditor): optional Yjs collaborative editing via a collab prop— adds the newcollab={{ url, token }}prop that opens a Yjs WebSocket relay and streams keystrokes between users. Needed by Milestone 8.14b896d—fix(collab): filter autocomplete and hover extensions in collab mode— disables two debounced ProseMirror extensions that crash withnull.matchesNode()when a remote Yjs update replaces the doc tree mid-debounce.e159b7b—fix(RichEditor): drop teardown-unsafe extensions in plain mode too— the same crash also fires without collab if the editor is remounted (e.g. draft/publishkeychange in Milestone 5) while the mouse is still hovering it. Filters those extensions in plain mode too. Needed by Milestone 5.The npm
0.6.1tarball is 2.3 MB; our committed0.6.1tarball is 3.6 MB — same"version": "0.6.1"inpackage.json, different build. So we can't swap to"@mieweb/ui": "0.6.1"(or0.7.0) from the registry today without breaking Milestones 5 and 8.What it takes to drop
vendor/mieweb-ui.tgzin a follow-upMerging mieweb/ui#344 alone is not enough. Two steps in
mieweb/ui:mainand publish it:npm version 0.7.1 && npm publish.Then in this repo, one commit:
and delete
vendor/mieweb-ui.tgz,scripts/ensure-ui-build.mjs, thepostinstall+setup:uiscripts, and (optionally) thevendor/uisubmodule +.gitmodulesentry.Shortcut around a publish:
"@mieweb/ui": "github:mieweb/ui#<merge-sha>"— installs directly from git. It doesn't work today becausemieweb/ui'sscripts.prepareonly builds CSS, not the JS bundle (that lives inscripts.build, whichpreparedoesn't call). A one-line change inmieweb/ui(foldbuildintoprepare) would unblock this if a formal release turns out to be inconvenient.Milestone 5 — Composer → RichEditor (Kerebron)
RichEditorfrom@mieweb/ui/kerebron(WYSIWYG ProseMirror, markdown in/out);@kerebron/*peer deps installed; wasm grammars served at/kerebron-wasmvia a small inline Vite plugin (dev middleware + dist copy)key) and submit updates instead of creatingMilestone 6 — Feed → SuperChat panel
SuperChatConversation; feed renders<SuperChat order="desc" readOnly virtualized>with code/image/mermaid render pluginsonMessageEdited→huddle.updatePostMilestone 7 — Verify
meteor-backend/tests/plan-gate.test.ts: 14 integration tests (gates + drafts); full backend integration suite 137/137Milestone 8 — Yjs Live-Sync
RichEditornow supports live collaborative sync via Yjs WebSocket provider (mieweb/ui#344)Test stability fix — shared-feed cross-session tests
The cross-session sync specs (
realtime/huddle-posts.spec.ts,huddle/yjs-collab-editing.spec.ts) were comparing two disjoint feeds — each logged-in user defaults to their private Personal team, so posts from one user were never visible to the other and the tests failed against a correctly-behaving app. Addedtests/e2e/fixtures/team.tswithselectSharedTestTeam(), which switches both sessions onto the seed teamTEST01(Test Team Alpha) vialocalStorage+ reload before the assertions. Fixes 4 previously-failing tests; full suite now 133/133.