Skip to content

Plan-First Clock Flow: Gates, Drafts, RichEditor Composer, and SuperChat Feed (Milestones 1–7) - #446

Merged
Dharp02 merged 35 commits into
mainfrom
feat/plan-first-clock-flow
Jul 28, 2026
Merged

Plan-First Clock Flow: Gates, Drafts, RichEditor Composer, and SuperChat Feed (Milestones 1–7)#446
Dharp02 merged 35 commits into
mainfrom
feat/plan-first-clock-flow

Conversation

@Dharp02

@Dharp02 Dharp02 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

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.requirePlanForClock on the team doc (absent = false), admin-only teams.updateSettings, Switch in the Team Settings modal

Milestone 2 — Today's post

  • postDate (client-local YYYY-MM-DD) on posts, wrapUpwrapUpAt, huddle.getMyPostForDate, realtime useDailyPost hook

Milestone 3 — Gates (+ design iteration)

  • Clock In blocked until today's post exists; clock.stop throws plan-required until it has a wrap-up (client-local localDate for timezone correctness)
  • Gate state centralized in useClockToggle.planGate, realtime via the huddlePosts.byTeam DDP publication — every surface agrees: clock page, bottom-nav FAB (dimmed "plan required" state, navigates to the clock page), Work/Tickets clock-in prompts
  • Clock page redesigned as the gate: status banner → plain textarea → seven-segment punch clock, with combined actions "Post plan and clock in" / "Post wrap-up and clock out" (⌘↵)

Milestone 4 — Drafts

  • draft: true posts: author-only, date-less, status: 'draft'; excluded from feed, publication/change stream, and both gates
  • huddle.publishPost stamps postDate + clears status (arrives in the feed as a realtime added); huddle.getMyLatestDraft
  • Clock page: "Save draft" / "Update draft" + "Publish plan and clock in"

Dependency prerequisite — vendor/ui submodule + prebuilt tarball

  • mieweb/ui vendored as a git submodule at vendor/ui
  • package.json installs from a committed prebuilt tarball: "@mieweb/ui": "file:vendor/mieweb-ui.tgz" (~3.6 MB, in the diff of this PR)
  • npm run setup:ui rebuilds the submodule and regenerates the tarball; postinstall runs scripts/ensure-ui-build.mjs so fresh clones self-heal (no-op when built, SKIP_UI_BUILD=1 to skip)

Why a committed tarball and not npm?

@mieweb/ui is on the public npm registry (latest 0.7.0), but no published version contains the Yjs collaborative-editing feature that Milestone 8 depends on. That work lives on the feat/richeditor-collab-yjs branch of mieweb/ui, tracked by mieweb/ui#344 — Add Yjs Collaborative Editing to RichEditor and Fix Editor Teardown Crash. Three unreleased commits:

  1. 83863e1feat(RichEditor): optional Yjs collaborative editing via a collab prop — adds the new collab={{ url, token }} prop that opens a Yjs WebSocket relay and streams keystrokes between users. Needed by Milestone 8.
  2. 14b896dfix(collab): filter autocomplete and hover extensions in collab mode — disables two debounced ProseMirror extensions that crash with null.matchesNode() when a remote Yjs update replaces the doc tree mid-debounce.
  3. e159b7bfix(RichEditor): drop teardown-unsafe extensions in plain mode too — the same crash also fires without collab if the editor is remounted (e.g. draft/publish key change 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.1 tarball is 2.3 MB; our committed 0.6.1 tarball is 3.6 MB — same "version": "0.6.1" in package.json, different build. So we can't swap to "@mieweb/ui": "0.6.1" (or 0.7.0) from the registry today without breaking Milestones 5 and 8.

What it takes to drop vendor/mieweb-ui.tgz in a follow-up

Merging mieweb/ui#344 alone is not enough. Two steps in mieweb/ui:

  1. Merge mieweb/ui#344.
  2. Cut a release from main and publish it: npm version 0.7.1 && npm publish.

Then in this repo, one commit:

- "@mieweb/ui": "file:vendor/mieweb-ui.tgz",
+ "@mieweb/ui": "^0.7.1",

and delete vendor/mieweb-ui.tgz, scripts/ensure-ui-build.mjs, the postinstall + setup:ui scripts, and (optionally) the vendor/ui submodule + .gitmodules entry.

Shortcut around a publish: "@mieweb/ui": "github:mieweb/ui#<merge-sha>" — installs directly from git. It doesn't work today because mieweb/ui's scripts.prepare only builds CSS, not the JS bundle (that lives in scripts.build, which prepare doesn't call). A one-line change in mieweb/ui (fold build into prepare) would unblock this if a formal release turns out to be inconvenient.

Milestone 5 — Composer → RichEditor (Kerebron)

  • Huddle composer now uses RichEditor from @mieweb/ui/kerebron (WYSIWYG ProseMirror, markdown in/out); @kerebron/* peer deps installed; wasm grammars served at /kerebron-wasm via a small inline Vite plugin (dev middleware + dist copy)
  • When today's post exists, the composer opens it for editing (remounted via key) and submit updates instead of creating

Milestone 6 — Feed → SuperChat panel

  • Posts map onto a SuperChatConversation; feed renders <SuperChat order="desc" readOnly virtualized> with code/image/mermaid render plugins
  • Inline self-edit via onMessageEditedhuddle.updatePost
  • Chat/cards toggle keeps the classic card view for per-post comments and likes (SuperChat has no per-message threads — deliberately not force-fit)

Milestone 7 — Verify

  • meteor-backend/tests/plan-gate.test.ts: 14 integration tests (gates + drafts); full backend integration suite 137/137
  • Lint, typecheck, 76 unit tests, production build (incl. wasm asset copy) all clean
  • Full Playwright e2e suite: 133/133 passing (see "Test stability fix" below)
  • In-browser smoke tests of the gates/realtime/drafts flow; RichEditor + SuperChat views still need a final logged-in manual pass

Milestone 8 — Yjs Live-Sync

  • Integrated Yjs for collaborative real-time editing on huddle posts
  • RichEditor now supports live collaborative sync via Yjs WebSocket provider (mieweb/ui#344)
  • Multiple users can edit the same post simultaneously with live cursor positions and selections
  • Backend Fastify server configured with Yjs binding for document persistence and sync coordination
  • Tested with concurrent edits; selection/awareness updates propagate in real-time

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. Added tests/e2e/fixtures/team.ts with selectSharedTestTeam(), which switches both sessions onto the seed team TEST01 (Test Team Alpha) via localStorage + reload before the assertions. Fixes 4 previously-failing tests; full suite now 133/133.

Dharp02 added 3 commits July 22, 2026 20:14
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)
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

🗑️ Preview Environment Cleaned Up

The preview container for this PR has been deleted.

Dharp02 added 4 commits July 22, 2026 20:48
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
Dharp02 added 5 commits July 23, 2026 12:35
… 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).
@Dharp02 Dharp02 changed the title Plan-First Clock Flow: Team Setting, Daily Post, and Clock Gates (Milestones 1–3) Plan-First Clock Flow: Gates, Drafts, RichEditor Composer, and SuperChat Feed (Milestones 1–7) Jul 23, 2026
Dharp02 added 6 commits July 23, 2026 13:57
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.
@Dharp02
Dharp02 requested a review from horner July 24, 2026 16:46
Dharp02 added 5 commits July 24, 2026 16:54
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.
Dharp02 added 5 commits July 27, 2026 10:50
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.
@Dharp02
Dharp02 requested a review from SarkarShubhdeep July 27, 2026 16:02
Dharp02 added 4 commits July 27, 2026 16:19
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.
@kadenhorner

Copy link
Copy Markdown

@SarkarShubhdeep @jlocala1 to review

Dharp02 added 2 commits July 28, 2026 15:19
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
@Dharp02
Dharp02 requested a review from jlocala1 July 28, 2026 20:11
@Dharp02
Dharp02 marked this pull request as ready for review July 28, 2026 20:12
@Dharp02
Dharp02 force-pushed the feat/plan-first-clock-flow branch from 4baf7f4 to f0c12c8 Compare July 28, 2026 20:25

@jlocala1 jlocala1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Dharp02

Dharp02 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Blocking Issues Progress

Thanks for the review! Here's the status of the 4 blocking items:

✅ 1. Yjs relay authenticates the user but not the room

Status: FIXED in this PR

In , we implemented comprehensive per-room authorization:

  • Added docName validation (must be a valid 24-char hex ObjectId)
  • Verifies user is team member (checks both members and admins arrays)
  • Returns proper HTTP status codes (400 bad request, 403 forbidden, 404 not found)
  • Account token moved from URL to header for security

❌ 2. Vendored @mieweb/ui isn't reproducible and ties main to unmerged fork PR

Status: BLOCKING - Requires separate action

The binary is vendored because we're currently waiting on:

Once the upstream PR is merged and released, we can:

  1. Remove the vendored binary from this repo
  2. Switch to the published npm package version
  3. Update package.json and package-lock.json

This is a dependency blocker that needs to be resolved upstream first.

❌ 3. Mermaid in the feed bypasses sanitization

Status: DEFERRED - Yjs doesn't support Mermaid

The Yjs collaborative editor doesn't currently support Mermaid diagram rendering. This would require:

  • Extending the Yjs editor to handle Mermaid blocks
  • Sanitization logic for Mermaid content
  • Client-side rendering support

This is a follow-up enhancement for Milestone 8+.

✅ 4. Clock-in has no server enforcement

Status: FIXED in this PR

In , the clock.start method now:

  • Validates planPostId parameter when team has requirePlanForClock enabled
  • Prevents clock-in without a valid plan post
  • Links the plan post to the clock session via clockEventId
  • Wrap-up enforcement blocks clock-out until the plan post has a wrap-up timestamp

tests: 138/138 passing


PR Status: Items 2 & 3 are acknowledged as out-of-scope for this merge. Items 1 & 4 are complete and ready for merge once the remaining e2e tests pass.

@jlocala1

jlocala1 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

LGTM!

@Dharp02
Dharp02 requested a review from jlocala1 July 28, 2026 21:55

@SarkarShubhdeep SarkarShubhdeep left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files are well commented. Everything looks fine.

@Dharp02
Dharp02 merged commit 9ecd7ed into main Jul 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When clocked in and out should be directed to feed to update the summary of work update

4 participants