Skip to content

feat(shared): add meeting details v2 view-model resolvers - #2909

Merged
nunoeufrasio merged 12 commits into
feat/meeting-details-v2from
feat/issue-2876
Oct 4, 2026
Merged

nunoeufrasio merged 12 commits into
feat/meeting-details-v2from
feat/issue-2876

Conversation

@nunoeufrasio

@nunoeufrasio nunoeufrasio commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the pure view-model resolvers behind the meeting details V2 redesign. This is foundation work — nothing consumes these yet; the V2 components land on top of them in later PRs.

Every resolver is total and deterministic: each returns a named member of its result type for every input combination, and none reads the clock (now is always a parameter). That is what lets the whole cross product of (time × viewer × privacy × artifact access × RSVP tracking) be asserted in a table rather than discovered in a template.

V1 is untouched. meeting-join.component.ts is byte-identical, and no app or server code changes in this PR.

What's here

File Change
interfaces/meeting-view-model.interface.ts New — MeetingTimeState, MeetingViewerRole, ActionSlotKind (9 kinds), MeetingPrivacyState, MeetingViewerContext, ActionSlotInput, MeetingSectionVisibilityInput, MeetingSectionVisibility
utils/meeting-view-model.utils.ts New — resolveTimeState, resolveViewerRole, resolvePrivacy, resolveActionSlot, resolveVisibleSections
utils/meeting-view-model.utils.spec.ts New — 56 tests
utils/meeting.utils.ts Hoisted MEETING_END_BUFFER_MS above its four consumers; canJoinMeeting and hasMeetingEnded take an optional trailing now = new Date() so the resolvers can be tested without faking time
docs/architecture/shared/package-architecture.md Documents the import '@angular/compiler'; prelude rule that shared specs reaching Angular declarations depend on
interfaces/index.ts, utils/index.ts Barrel exports

Decisions worth a reviewer's attention

The RSVP hard gate is enforced in two places on purpose. Meeting.is_invite_responses_enabled is only true for meetings created after the January 2024 invite-responses release. When it isn't true, rsvpSummary, rsvpRosterFilter and rsvpAvatarBadges are each false — the rule is "no RSVP UI anywhere", not "no RSVP card" — and resolveActionSlot can never return rsvp.

hasArtifactAccess is shared between the rail and the section resolvers. When they each carried their own rule, an organizer without PublicPastMeetingResponse.full_access resolved to a tools rail pointing at sections the same view model had just hidden. One cross-resolver test pins the cell where they previously disagreed.

An absent visibility reads as private. Meeting.visibility is nullable and both privacy helpers fall through to the public label and the globe icon on null — which would put "Public" under a globe in the header while the rail told the same viewer they need an invitation. Resolved fail-closed so the header and the rail cannot disagree.

resolveTimeState describes the occurrence it is handed, not the series. Both delegates fall back to the series' start_time when the occurrence is nullish, so a long-running series whose first occurrence is months past would resolve to ended while future occurrences exist. The caller owns occurrence selection — the V2 page has an occurrence strip and knows which occurrence it is showing — so the contract is documented and pinned by a test rather than hidden inside the resolver.

Two ActionSlotKind members exist to name V1's silent failures. invitation-required (signed-in outsider on a restricted meeting) and rsvp-unavailable (registrant on a pre-2024 meeting) both render as an empty action rail in V1 today. Naming them is much of the point of this redesign.

No host viewer tier. The epic's viewer axis names one, but nothing on the meeting payload distinguishes a host from an organizer today, and adding it would put a state in the view model that no resolver can ever return.

Deliberately out of scope

MEETING_END_BUFFER_MS stays in meeting.utils.ts rather than moving to constants/. It was raised in review as an optional follow-up and judged not worth expanding this commit's scope for.

Testing

  • 56 new tests, all passing; 235 shared tests green overall.
  • Every new test proven binding by mutation: the source was mutated, the test confirmed failing, and the mutation confirmed landed via git diff.
  • yarn check-types, yarn format, yarn lint (0 errors) and yarn build all clean.
  • ./check-headers.sh passes.

Refs #2876

🤖 Generated with Claude Code

Adds the pure view-model layer the meeting details V2 components will be
built on (#2876). Nothing consumes it yet — V1 is untouched.

New in @lfx-one/shared:
- meeting-view-model.interface.ts — MeetingTimeState, MeetingViewerRole,
  ActionSlotKind and the resolver input/output shapes.
- meeting-view-model.utils.ts — resolveTimeState, resolveViewerRole,
  resolvePrivacy, resolveActionSlot, resolveVisibleSections. Every
  function is total and deterministic; none reads the clock.

meeting.utils.ts changes, all additive:
- MEETING_END_BUFFER_MS moves above its four consumers, which each
  inlined the same forty-minute literal. No literal remains.
- canJoinMeeting and hasMeetingEnded take an optional trailing
  `now = new Date()`, matching isOccurrencePast in the same file. No
  call site changes and every existing spec stays green; this is what
  lets resolveTimeState reuse the real join window deterministically.

The resolvers name the three states V1 renders as an empty rail:
invitation-required, no-access and rsvp-unavailable. RSVP is hard-gated
on the normalized is_invite_responses_enabled — resolveActionSlot can
never return 'rsvp' and resolveVisibleSections zeroes every RSVP surface
when it is false.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Addresses post-commit review findings on the meeting details V2 view
model. `resolveEndedActionSlot` gave an organizer `tools`
unconditionally while `resolveVisibleSections` gated `tools` on
`past_meeting_full_access` alone, so an organizer without that flag
resolved to a tools rail pointing at sections the same view model had
hidden. Both now share one `hasArtifactAccess` rule, and the past
roster is gated with the rest of the past content rather than on
membership alone.

Also documents `resolveTimeState`'s nullish-occurrence contract — it
describes the occurrence it is handed, falling back to the series'
start_time — and pins it with a test, since the caller owns occurrence
selection.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Addresses the whole-branch review of the meeting details V2 view model.

The access-gate comments named `past_meeting_full_access`, which greps
to nothing. The field is `full_access` on `PublicPastMeetingResponse`,
held client-side as `pastMeetingFullAccess`; the comments now say so,
since they are the only mapping a V2 component author has.

`resolvePrivacy` also inherited the label and icon helpers' fallthrough
for a null `visibility`, producing "Public" under a globe while
`openToPublic` stayed false and the rail told the same viewer they
needed an invitation. An absent visibility now reads private across
every field, so the header and the rail cannot disagree.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
A Vitest spec under `packages/shared/src/` whose import graph reaches an
Angular declaration — `meeting.utils.ts` pulls `HttpParams` from
`@angular/common/http` — throws at import time without
`import '@angular/compiler';` first. Five specs already carry that
prelude as folklore; the package architecture doc now states the rule
next to the related barrel-import rule.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
@nunoeufrasio
nunoeufrasio requested a review from a team as a code owner September 23, 2026 22:59
Copilot AI balanced review requested due to automatic review settings September 23, 2026 22:59
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 523de5e7-14e9-4d2b-b6c1-f88e7f0948d3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Additive shared library and tests with backward-compatible optional parameters on meeting timing helpers; no app or server integration yet.

Overview
Adds foundation-only shared view-model logic for meeting details V2: new types (MeetingTimeState, viewer roles, ActionSlotKind, section visibility) and pure resolvers that map (time × viewer × privacy × artifact access × RSVP tracking) to a single action-rail kind and per-section booleans. Nothing in the app wires these up yet.

meeting.utils is adjusted so join/end timing stays consistent: MEETING_END_BUFFER_MS is hoisted and reused everywhere, and canJoinMeeting / hasMeetingEnded accept an optional now for deterministic tests and the new resolveTimeState.

Coverage is a large Vitest suite (including a full action-slot matrix). Docs note that packages/shared specs that transitively touch Angular must import '@angular/compiler' first; meeting-calendar.utils.spec.ts gets that prelude comment.

Reviewed by Cursor Bugbot for commit 1a79725. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The action-slot tests do not assert the complete expected matrix required by the linked issue.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds shared, deterministic view-model resolvers for the future Meeting Details V2 page.

Changes:

  • Adds meeting state, privacy, action-slot, and section-visibility resolvers.
  • Makes meeting timing helpers deterministic via an injectable clock.
  • Adds Vitest coverage, barrel exports, and test setup documentation.
File Description
packages/​shared/​src/​utils/​meeting.utils.ts Centralizes the end buffer and adds clock overrides.
packages/​shared/​src/​utils/​meeting-view-model.utils.ts Implements the V2 view-model resolvers.
packages/​shared/​src/​utils/​meeting-view-model.utils.spec.ts Tests resolver behavior and state combinations.
packages/​shared/​src/​utils/​index.ts Exports the new utilities.
packages/​shared/​src/​interfaces/​meeting-view-model.interface.ts Defines resolver inputs and outputs.
packages/​shared/​src/​interfaces/​index.ts Exports the new interfaces.
docs/​architecture/​shared/​package-architecture.md Documents Angular compiler setup for shared specs.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/shared/src/utils/meeting-view-model.utils.spec.ts Outdated
Comment thread docs/architecture/shared/package-architecture.md
Replaces the totality sweep in the resolveActionSlot spec with a table
that writes out the expected kind for all 48 (time, viewer, privacy)
tuples across the four (fullAccess, inviteResponsesEnabled) pairs. The
sweep only asserted that each result was some valid ActionSlotKind, so
an unlisted cell could change to another kind and still pass, and it
never exercised private + restricted at all.

Two guard tests keep the table honest: every tuple appears exactly
once, and every ActionSlotKind is reachable. Proven binding per R07 by
returning 'none' for a live outsider on a private restricted meeting;
exactly that row failed.

Also adds the missing why-comment to the compiler prelude in
meeting-calendar.utils.spec.ts, which the package-architecture rule
this PR introduces requires.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI review requested due to automatic review settings September 24, 2026 12:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Anonymous visitors can resolve to authenticated-only post-meeting tools they cannot load.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Allowing visitors to see inaccessible past-meeting tools

packages/​shared/​src/​utils/​meeting-view-model.utils.ts:181

fullAccess is not sufficient to make an anonymous visitor artifact-capable. checkPastMeetingAccess returns true for every public/unrestricted past meeting before checking authentication (apps/lfx-one/src/server/helpers/meeting.helper.ts:273-289), while recordings, summaries, transcripts, participants, and attachments are fetched from required-auth /api/past-meetings/* routes (apps/lfx-one/src/server/middleware/auth.middleware.ts:81-83). Consequently this helper makes resolveEndedActionSlot return tools (and resolveVisibleSections().tools return true) for a visitor who cannot load any tool, as the matrix now explicitly asserts. Split public past-page content access from authenticated artifact-endpoint access, or at minimum require a non-visitor role for the tools action and section.

resolveActionSlot keyed joining on openToPublic (public AND
unrestricted), but in the app only self-registration needs that.
Reaching a non-open page already required the meeting password, so a
loaded page means the viewer holds the link, and an unrestricted
meeting lets anyone with the link join. Against V1 on main that
removed three things V1 allows (state matrix D-1 to D-3, signed off):

- D-1: every anonymous visitor in the join window now gets guest-join,
  whatever the privacy. On a restricted meeting the join-url endpoint
  matches the submitted email against the registrants, and this is
  how an invitee without an LFX session joins from their invite link.
  The resolver returned none and would have locked them out.
- D-2: a signed-in outsider on a live public-open meeting joins
  directly, as V1 lets them, instead of registering first.
- D-3: a signed-in outsider on a private unrestricted meeting joins
  when live and gets none before the window, instead of being told an
  invitation is required for a meeting that needs none.

openToPublic now gates registration only; the ActionSlotKind and
MeetingPrivacyState docs say so. D-4 (anonymous + ended public
meeting stays tools, rendered as sign-in until public artifact routes
exist) needed no resolver change.

The 48-row matrix is regenerated from the new rules and the named
cases updated. Proven binding per R07: restoring the old visitor rule
failed exactly the five cells it touches.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI review requested due to automatic review settings September 24, 2026 13:56
nunoeufrasio added a commit that referenced this pull request Sep 24, 2026
State matrix decisions D-1 to D-3 (applied to the resolver in PR #2909) change two descriptions here: guest-join now covers every anonymous visitor in the join window, whatever the privacy, with the server matching the email on restricted meetings; and invitation-required is the restricted-meeting outsider only, since an outsider holding the link to a private unrestricted meeting can join.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The visibility model currently exposes inconsistent RSVP state, hides authorized host-key access, and offers authenticated-only artifact tools to visitors.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Gate RSVP surfaces on content visibility

packages/​shared/​src/​utils/​meeting-view-model.utils.ts:145

rsvpVisible can remain true for an ended registrant whose fullAccess is false, while contentVisible and therefore people are false. That enables the roster filter and per-avatar badges on a roster this same view model hides (and enables a summary whose counts are absent from the reduced past-meeting payload). Gate RSVP surfaces on contentVisible as well.

Medium severity Allow authorized non-registrants to view host key

packages/​shared/​src/​utils/​meeting-view-model.utils.ts:149

This hides the join-details region from authorized co-hosts and project/committee writers who are neither registrants nor organizers. The BFF deliberately supports organizer: false with can_view_host_key: true (see public-meeting.controller.spec.ts:259-272), and the existing page renders the host key from that server-verified field. Since this interface says joinDetails includes the host key, add the host-key authorization signal to the visibility model or split host-key visibility from member-only join details.

Medium severity Do not offer protected artifact tools to anonymous visitors

packages/​shared/​src/​utils/​meeting-view-model.utils.ts:169

An anonymous visitor can have fullAccess=true for a public unrestricted past meeting (checkPastMeetingAccess returns true before authentication), so this returns tools. However recording, transcript, summary, and attachment reads all use protected /api/past-meetings/... endpoints; the current page explicitly requires authentication before fetching or rendering them. Distinguish authenticated artifact access from public basic-content access so visitors are not offered nonfunctional tools.

Comment thread packages/shared/src/utils/meeting-view-model.utils.ts Outdated
Address review comments from the reviewers on PR #2909:

- meeting-view-model.utils.ts, meeting-view-model.utils.spec.ts: stop
  presenting specs/010-meeting-details-redesign/state-matrix.md as an
  existing source of truth. It lands with E0-01 in PR #2935 and is not
  on main yet; both comments now name the spec's MATRIX table as the
  executable source and mark the written matrix as pending (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Anonymous viewers are marked as able to see materials even though the materials endpoint requires authentication.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Hide Materials from unauthenticated upcoming viewers

packages/​shared/​src/​utils/​meeting-view-model.utils.ts:152

The linked E0-01 state matrix marks Materials as unavailable to visitor because attachments are fetched from the authenticated /api/meetings/:id/attachments route; the current meeting service confirms that path. Returning true here for an upcoming anonymous viewer would make V2 render a section it cannot populate. Keep Materials hidden for visitors until the planned public materials endpoint exists.

@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Commit: f325ad6

Changes made

Threads resolved

1 of 1.

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:39

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ad04bba. Configure here.

Comment thread packages/shared/src/utils/meeting-view-model.utils.ts
nunoeufrasio added a commit that referenced this pull request Oct 1, 2026
Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md: adds a View-scoped state section. The selected
  occurrence comes from ?occurrence= (else the current or next active
  one). The viewer's own RSVP comes from
  GET /api/meetings/:uid/rsvp/me?occurrenceId=, because the public
  detail payload does not populate my_rsvp.
- data-model.md, Counts: past counts on the public page come from
  GET /api/past-meetings/:uid/participants and only for signed-in
  viewers. GET /api/past-meetings/:uid does exist
  (past-meetings.route.ts:65) and fills count fields, but the public
  page does not call it.
- state-matrix.md: a Manage-role viewer opening /meetings/:id directly
  is legal (#2251 changed only the card's CTA; routes are not
  role-guarded), so it moves out of the illegal list. SC-002 now names
  the Reachability section's redirects only.

Also aligns the RSVP visibility row with PR #2909: RSVP surfaces hide
with the roster on an ended meeting without artifact access.

Resolves 3 review threads.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Past-meeting responses do not expose the registrant status required to resolve viewer roles correctly.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread packages/shared/src/interfaces/meeting-view-model.interface.ts Outdated
Address review comments from the reviewers on PR #2909:

- meeting-view-model.utils.ts: the past roster keyed on the viewer
  role, but the past endpoint never sets Meeting.invited (it folds
  registrant status into full_access in checkPastMeetingAccess), so a
  past registrant resolved as outsider and lost the roster meant for
  them. For an ended meeting, people now keys on "signed in with
  artifact access", matching v1, which loads past participants for any
  authenticated viewer with access; anonymous viewers still get none.
  New test pins it; proven binding per R07 (restoring the role-based
  rule failed exactly it).
- meeting-view-model.interface.ts: MeetingViewerContext.invited now
  documents that only the upcoming endpoint populates it (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 10:05
nunoeufrasio added a commit that referenced this pull request Oct 1, 2026
Mirrors the fix to PR #2909's resolveVisibleSections, prompted by a
review thread there: the past payload never sets Meeting.invited (the
BFF folds registrant status into full_access), so a past registrant
arrives as an outsider. FR-032, the section-visibility table and the
data model now say the ended-meeting roster shows for any signed-in
viewer with artifact access, never for anonymous viewers, and that
past sections key on fullAccess rather than the role.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The resolvers match the documented state matrix, preserve existing behavior, and have comprehensive deterministic coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The public contract misstates host-key gating and permits an impossible nullable privacy result.

Review effort: Balanced
Findings: 1 Medium severity · 2 Low severity

Open (3)

Comment thread packages/shared/src/interfaces/meeting-view-model.interface.ts Outdated
Comment thread packages/shared/src/interfaces/meeting-view-model.interface.ts Outdated
Comment thread packages/shared/src/interfaces/meeting-view-model.interface.ts Outdated
Address review comments from the reviewers on PR #2909 (all per
copilot-pull-request-reviewer[bot]):

- meeting-view-model.interface.ts: MeetingPrivacyState.visibility is
  MeetingVisibility, not MeetingVisibility | null. resolvePrivacy reads
  an absent visibility as private, so null is never produced, and the
  wider type let callers build a state the fail-closed resolver cannot.
- meeting-view-model.interface.ts: the viewer-role doc no longer claims
  the payload cannot distinguish a host. Host-key entitlement is
  orthogonal to the role and already on the payload
  (can_view_host_key / host_key, set for the FGA host relation,
  co-hosts included); read it directly, never via organizer.
- meeting-view-model.interface.ts: joinDetails covers the join link
  only. The host key is not gated by it, since a non-organizer co-host
  resolves as outsider.

Also points the spec-path comments at specs/011-meeting-details-redesign:
main took 010 for specs/010-shared-ghcr-cleanup (#3202).

Resolves 3 review threads.

Refs #2876

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:42
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Commit: 1a79725 (plus the main sync a97e762)

Changes made (all per copilot-pull-request-reviewer[bot])

  • meeting-view-model.interface.ts:
    • MeetingPrivacyState.visibility is now non-nullable, since the resolver never produces null.
    • The role doc now says host-key entitlement is orthogonal to the role (can_view_host_key / host_key).
    • joinDetails covers the join link only; the host key renders from the payload fields.
  • Spec-path comments now point at specs/011-meeting-details-redesign (main took 010 in ci: use shared GHCR image cleanup workflow #3202).

Threads resolved

3 of 3. Checks: type-check passes; 110 view-model tests pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

The public people visibility documentation contradicts the resolver’s past-meeting behavior.

Review effort: Balanced
Findings: None

Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Low severity Update people contract for ended outsiders with artifact access

packages/​shared/​src/​interfaces/​meeting-view-model.interface.ts:147

This contract still says people is false for every outsider, but the resolver now deliberately returns true for an ended, signed-in outsider with artifact access (the past payload loses registrant status and uses full_access). Leaving the public interface documentation role-based contradicts the fixed behavior and can cause the V2 adapter to hide the past-participants roster. Document the upcoming/live and ended rules separately.

@nunoeufrasio
nunoeufrasio changed the base branch from main to feat/meeting-details-v2 October 4, 2026 22:53
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Base changed to feat/meeting-details-v2. The meeting details V2 epic (#1765) now lands in that integration branch instead of main. Merging this PR ships nothing to production: main only changes when the whole project is finished and one final PR, feat/meeting-details-v2 → main, merges. Please review and merge this PR into the integration branch as usual.

nunoeufrasio added a commit that referenced this pull request Oct 4, 2026
…nts (#2935)

* docs(meetings): add the verified meeting details state matrix

Adds the state matrix #1766 deferred: seven axes plus page status, the reachability rules that make a combination legal or a not-found redirect, what V1 renders on main today for every legal action-slot cell, and what PR #2909's resolveActionSlot proposes for V2.

Built from the Cowork state assessment and implementation plan, then re-verified against main @ 1ee3530; where they disagree main wins. Adds an arrival-credential axis (password) the plan lacks, because every restricted-meeting outsider or visitor state is only reachable through an invite link's ?password=.

Records four divergences (D-1..D-4) where the proposed resolver would remove something V1 allows, most importantly anonymous invitees joining a restricted meeting through the guest form. They need sign-off before E2-01 builds on the resolver.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): add requirements and data model for meeting details v2

Completes the #1766 deliverables that spec.md (PR #2914) deferred:

- requirements.md: FR-001 to FR-063 and SC-001 to SC-008, with a
  traceability table so every Phase 1 and Phase 2 plan ID cites at
  least one requirement.
- data-model.md: payload -> axis mapping, what the E0-02 view model
  encodes and the invariants that tie its resolvers together, the
  count sources left after GH-1731, the identifier formats that are
  easy to mix up, and the upstream U-01..U-08 blockers.
- checklists/requirements.md, following specs/008 and 009.
- state-matrix.md records D-1 to D-4 as signed off and shows the
  resolver after them (applied in PR #2909).

contracts/ is deliberately left to the PRs that build each endpoint
(E3-03, E4-04, E6-01), where schema and code are reviewed together.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): correct not-found triggers and join-details visibility

From the pre-PR review:

- A 404 on the upcoming lookup does not redirect; it falls back to the
  past-meeting lookup (V1 TS:862-880), and only a 404 / 403 / 400 from
  that fallback redirects. The reachability note and the not-found row
  said 400/403/404 redirect directly, which would have had V2 drop the
  fallback and 404 plain numeric ids of past meetings.
- FR-001 now scopes "anonymous visitors get V1" to the rollout, as R05
  does, so it no longer contradicts the visitor requirements (FR-021,
  FR-025, FR-028, FR-031, SC-003).
- The section-visibility table split "Join details (host key)" into
  join details (registrants and organizers, not once ended), matching
  resolveVisibleSections, and the host key (organizer, in window).

Also records the U-01..U-08 tracker issues (#2927-#2934), filed today.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- requirements.md FR-031: materials are required only where content
  is visible; an ended meeting without artifact access keeps them
  hidden for every viewer, matching resolveVisibleSections.
- requirements.md FR-042, state-matrix.md, data-model.md: host-key
  access is orthogonal to the viewer role. The BFF returns host_key to
  whoever holds the FGA host relation (meeting.helper.ts:162-163),
  which includes co-hosts who are not organizers. "Host without
  organizer" is no longer listed as illegal, and the host-key row no
  longer depends on the join-details section.
- state-matrix.md axis G, reachability, FR-007: the meeting password
  also arrives through router navigation state when the composer opens
  a meeting it just created (statePassword()), not only ?password=.

The license-header thread is answered without a change: the deviation
is recorded in spec.md and follows specs/008 and 009.

Resolves 5 review threads.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md: adds a View-scoped state section. The selected
  occurrence comes from ?occurrence= (else the current or next active
  one). The viewer's own RSVP comes from
  GET /api/meetings/:uid/rsvp/me?occurrenceId=, because the public
  detail payload does not populate my_rsvp.
- data-model.md, Counts: past counts on the public page come from
  GET /api/past-meetings/:uid/participants and only for signed-in
  viewers. GET /api/past-meetings/:uid does exist
  (past-meetings.route.ts:65) and fills count fields, but the public
  page does not call it.
- state-matrix.md: a Manage-role viewer opening /meetings/:id directly
  is legal (#2251 changed only the card's CTA; routes are not
  role-guarded), so it moves out of the illegal list. SC-002 now names
  the Reachability section's redirects only.

Also aligns the RSVP visibility row with PR #2909: RSVP surfaces hide
with the roster on an ended meeting without artifact access.

Resolves 3 review threads.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): align the past roster rule with the view model

Mirrors the fix to PR #2909's resolveVisibleSections, prompted by a
review thread there: the past payload never sets Meeting.invited (the
BFF folds registrant status into full_access), so a past registrant
arrives as an outsider. FR-032, the section-visibility table and the
data model now say the ended-meeting roster shows for any signed-in
viewer with artifact access, never for anonymous viewers, and that
past sections key on fullAccess rather than the role.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): add the occurrence actions main added to v1

main's V1 details page gained organizer single-occurrence actions
(#3040, #3206): "Edit this occurrence" (reschedule, title, agenda) and
"Cancel this occurrence", shown on a recurring meeting for the
selected upcoming occurrence. Adds FR-046 so V2 ports them, narrows
FR-051's new-scope list to whole-series edit/delete and the O-01..O-04
dialogs, and adds the row to the section-visibility table and the
traceability table.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md, Counts: the "no count source" conclusion is scoped to
  upcoming meetings. On a past meeting any signed-in viewer with access,
  outsider included, has counts through the participants list; only
  anonymous viewers have none, matching FR-032.
- data-model.md, View-scoped state and Identifiers: ?occurrence= is a
  Unix-millisecond start timestamp, matched against start_time
  (meeting-join.component.ts:1022-1027), not an occurrence ID. The RSVP
  request takes the matched occurrence's seconds-based occurrence_id,
  never the URL value.

Also moves the spec directory from 010 to 011: main took 010 for
specs/010-shared-ghcr-cleanup (#3202).

Resolves 2 review threads.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md, state-matrix.md: cadence is `!!meeting.recurrence`
  for upcoming payloads, not `!== null` (an omitted field is not a
  series). For past meetings, whose payload can omit recurrence, it is
  derived from the series (getMeetingSeriesUid, as V1 does at TS:1047)
  or the occurrence timeline.
- requirements.md FR-050: passcode and dial-in leave the permanent
  exclusion list; they are E7 work held back until U-04 / U-05 (FR-043).
- requirements.md FR-063, checklist: browser redirect coverage is
  scoped to the Reachability section's illegal rows; impossible input
  combinations (Axis D on non-ended, RSVP states with tracking off) are
  covered by resolver-level unit tests.
- requirements.md traceability: adds FR-047 (occurrence data: widened
  and paginated endpoint, past attendance after U-01), FR-048 (derived
  recording duration) and FR-054 (per-meeting guard on the admin past
  route, own PR per R06). Adds rows for E0-01, E0-03, E4-02, E5-01
  (shipped), N-03, E6-01/02/06, E7-03, E8-01/02/03, E9-03 and the
  S-01/S-02 spikes. Checked against every plan-ID heading in the plan.

Resolves 4 review threads.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 4, 2026
* docs(meetings): define the v2 meeting details testid contract

Adds specs/010-meeting-details-redesign/testid-contract.md: the
authoritative data-testid and data-* state list for the V2 meeting
details tree, written ahead of the components so every component PR and
the V2 E2E suite agree on the same selectors.

State lives in data-* attributes, never class names; dynamic testids key
on entity ids, never collection indexes. data-kind mirrors all nine
ActionSlotKind members rather than the six the issue body lists, which
predate the E0-02 viewer-state model. data-attendance is absent, not
"none", when invite responses are disabled. tools-summary-btn carries a
data-approval axis because V1 already renders that state as a badge.

No existing spec asserts on the V1 details tree: an exact-match search
for getByTestId across e2e/ finds no consumer among its 38 testids. The
two names that look like consumers are exercised on meeting-card via the
/meetings list, not on this page.

Points .claude/rules/development-rules.md at the contract so component
authors find it.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): correct the action-kind mapping in the testid contract

The note on the three kinds E0-02 added said one of them covers the
signed-in outsider on a restricted meeting. It does not: that state
resolves to invitation-required, one of the original six. The note now
states what each addition names as resolveActionSlot decides it:
guest-join, no-access (an ended meeting without artifact access) and
rsvp-unavailable.

Also closes three gaps in the same file:

- data-attendance and data-invitation had no value sets. They now
  mirror RegistrantAttendanceStatus and MeetingRegistrant.type.
- data-approval had no value for a summary that never required
  approval, which V1 renders with no badge. It is now not-required,
  not an absent attribute.
- V1 carries 56 distinct testids, not 38; the count missed the 18 bound
  through [attr.data-testid]. Re-verified that none is consumed by a
  spec visiting /meetings/:id.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): mark the spec.md cross-reference as pending #2914

The testid contract points at spec.md's silent-failure list, which lands with E0-01 (PR #2914) and is not on main yet. The reference now names the section and says where the file comes from, matching how this contract already marks its other unmerged siblings.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): align the testid contract with the signed-off join rules

State matrix decisions D-1 to D-3 (applied to the resolver in PR #2909) change two descriptions here: guest-join now covers every anonymous visitor in the join window, whatever the privacy, with the server matching the email on restricted meetings; and invitation-required is the restricted-meeting outsider only, since an outsider holding the link to a private unrestricted meeting can join.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): bring the testid contract up to date with main

main's V1 page gained organizer occurrence controls ("Edit this
occurrence", "Cancel this occurrence", #3040 and #3206). V2 must port
them, so the contract names their V2 testids:
meeting-occurrence-edit-button and meeting-occurrence-cancel-button.

The V1 inventory is recounted against main: 55 distinct testids (37
static, 18 bound), not 56. Re-verified that none is consumed by a spec
visiting /meetings/:id; meeting-title joins the shared-name table, as
its only consumer is a meeting-card spec on /meetings.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2913 review feedback

Address review comments from the reviewers on PR #2913 (all per
copilot-pull-request-reviewer[bot]):

- testid-contract.md: the status pill and privacy chip expose runtime
  state but had no state attributes, contradicting Rule 1. Adds
  meeting-status-pill[data-state] (MeetingTimeState) plus
  [data-my-rsvp] (registrant with tracking on only), and
  meeting-privacy-chip[data-visibility] / [data-restricted], mirroring
  MeetingPrivacyState.
- testid-contract.md: people rows are split by data source. Upcoming
  rows (MeetingRegistrant) keep data-attendance / data-invitation;
  past rows (PastMeetingParticipant, no RSVP and no type) get
  data-attended / data-invited. The identity-tier attribute waits for
  N-02.

Also moves the spec directory from 010 to 011: main took 010 for
specs/010-shared-ghcr-cleanup (#3202). The development-rules pointer
follows.

Resolves 2 review threads.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* fix(review): address PR #2913 review feedback

Address review comments from the reviewers on PR #2913:

- testid-contract.md: meeting-status-pill[data-my-rsvp] is now present
  for any invited viewer with RSVP tracking on, including an organizer
  who is also invited. The view model resolves that viewer as the
  organizer role and the action slot still returns rsvp for them, as
  V1 lets an invited organizer set and see their own RSVP. The
  attribute keys on Meeting.invited, not on the role (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #1768

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
@nunoeufrasio
nunoeufrasio merged commit 67e01a7 into feat/meeting-details-v2 Oct 4, 2026
18 checks passed
@nunoeufrasio
nunoeufrasio deleted the feat/issue-2876 branch October 4, 2026 23:06
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* docs(meetings): add the v2 rollout and retirement plan

Adds specs/011-meeting-details-redesign/rollout.md for V2-03. It
separates the two controls: the integration branch decides whether the
code is in production at all (nothing reaches main until release PR
#3249), and MEETING_V2_ENABLED_FLAG decides who sees V2 once it is.

Defines eight stages (build, release, internal testers, extended
testers, signed-in percentage, anonymous, soak, retire), each with who
decides and exit criteria. States that the code default never changes
and targeting is the switch. Gives the anonymous plan: a code change
that needs #2920's server-side evaluation, then an anonymous
LaunchDarkly context ramped by percentage. Adds what is measured, a
rollback table, and flag retirement: this page drops its read of the
shared flag, and the flag itself retires only with the composer.

The V1-deletion checklist is filed as #3266. The README's contents
table now lists the files that actually exist.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): reconcile v2 spec cross-references after the merge

The foundation PRs (#2909, #2910, #2911, #2913, #2914, #2935) now sit
together on feat/meeting-details-v2, so the "pending sibling, not on
main yet" notes they carried are stale. They become plain
cross-references in spec.md, state-matrix.md, data-model.md,
requirements.md, testid-contract.md, v2-scaffold.md and the
meeting-view-model resolver comments.

Also records the newly filed Phase 1 issues (#3250-#3265) in spec.md's
work order and requirements.md's traceability table, and the V1
retirement issue #3266.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): fix rollout plan gating and rollback guidance

Make #2920 the stage 4 entry gate rather than a release blocker,
scope R05 and FR-001 to the stages before the anonymous stage, give
the V1 rollback a targeted-revert path, and stop reusing the D-2 label.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): address review feedback on the rollout plan

Make FR-001 platform-neutral with the signed-in and post-hydration
rules scoped to their stages, limit stage 2 to signed-in states, define
the baseline once a V1 cohort is gone, and re-wrap a spec paragraph.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
…nts (#2935)

* docs(meetings): add the verified meeting details state matrix

Adds the state matrix #1766 deferred: seven axes plus page status, the reachability rules that make a combination legal or a not-found redirect, what V1 renders on main today for every legal action-slot cell, and what PR #2909's resolveActionSlot proposes for V2.

Built from the Cowork state assessment and implementation plan, then re-verified against main @ 1ee3530; where they disagree main wins. Adds an arrival-credential axis (password) the plan lacks, because every restricted-meeting outsider or visitor state is only reachable through an invite link's ?password=.

Records four divergences (D-1..D-4) where the proposed resolver would remove something V1 allows, most importantly anonymous invitees joining a restricted meeting through the guest form. They need sign-off before E2-01 builds on the resolver.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): add requirements and data model for meeting details v2

Completes the #1766 deliverables that spec.md (PR #2914) deferred:

- requirements.md: FR-001 to FR-063 and SC-001 to SC-008, with a
  traceability table so every Phase 1 and Phase 2 plan ID cites at
  least one requirement.
- data-model.md: payload -> axis mapping, what the E0-02 view model
  encodes and the invariants that tie its resolvers together, the
  count sources left after GH-1731, the identifier formats that are
  easy to mix up, and the upstream U-01..U-08 blockers.
- checklists/requirements.md, following specs/008 and 009.
- state-matrix.md records D-1 to D-4 as signed off and shows the
  resolver after them (applied in PR #2909).

contracts/ is deliberately left to the PRs that build each endpoint
(E3-03, E4-04, E6-01), where schema and code are reviewed together.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): correct not-found triggers and join-details visibility

From the pre-PR review:

- A 404 on the upcoming lookup does not redirect; it falls back to the
  past-meeting lookup (V1 TS:862-880), and only a 404 / 403 / 400 from
  that fallback redirects. The reachability note and the not-found row
  said 400/403/404 redirect directly, which would have had V2 drop the
  fallback and 404 plain numeric ids of past meetings.
- FR-001 now scopes "anonymous visitors get V1" to the rollout, as R05
  does, so it no longer contradicts the visitor requirements (FR-021,
  FR-025, FR-028, FR-031, SC-003).
- The section-visibility table split "Join details (host key)" into
  join details (registrants and organizers, not once ended), matching
  resolveVisibleSections, and the host key (organizer, in window).

Also records the U-01..U-08 tracker issues (#2927-#2934), filed today.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- requirements.md FR-031: materials are required only where content
  is visible; an ended meeting without artifact access keeps them
  hidden for every viewer, matching resolveVisibleSections.
- requirements.md FR-042, state-matrix.md, data-model.md: host-key
  access is orthogonal to the viewer role. The BFF returns host_key to
  whoever holds the FGA host relation (meeting.helper.ts:162-163),
  which includes co-hosts who are not organizers. "Host without
  organizer" is no longer listed as illegal, and the host-key row no
  longer depends on the join-details section.
- state-matrix.md axis G, reachability, FR-007: the meeting password
  also arrives through router navigation state when the composer opens
  a meeting it just created (statePassword()), not only ?password=.

The license-header thread is answered without a change: the deviation
is recorded in spec.md and follows specs/008 and 009.

Resolves 5 review threads.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md: adds a View-scoped state section. The selected
  occurrence comes from ?occurrence= (else the current or next active
  one). The viewer's own RSVP comes from
  GET /api/meetings/:uid/rsvp/me?occurrenceId=, because the public
  detail payload does not populate my_rsvp.
- data-model.md, Counts: past counts on the public page come from
  GET /api/past-meetings/:uid/participants and only for signed-in
  viewers. GET /api/past-meetings/:uid does exist
  (past-meetings.route.ts:65) and fills count fields, but the public
  page does not call it.
- state-matrix.md: a Manage-role viewer opening /meetings/:id directly
  is legal (#2251 changed only the card's CTA; routes are not
  role-guarded), so it moves out of the illegal list. SC-002 now names
  the Reachability section's redirects only.

Also aligns the RSVP visibility row with PR #2909: RSVP surfaces hide
with the roster on an ended meeting without artifact access.

Resolves 3 review threads.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): align the past roster rule with the view model

Mirrors the fix to PR #2909's resolveVisibleSections, prompted by a
review thread there: the past payload never sets Meeting.invited (the
BFF folds registrant status into full_access), so a past registrant
arrives as an outsider. FR-032, the section-visibility table and the
data model now say the ended-meeting roster shows for any signed-in
viewer with artifact access, never for anonymous viewers, and that
past sections key on fullAccess rather than the role.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): add the occurrence actions main added to v1

main's V1 details page gained organizer single-occurrence actions
(#3040, #3206): "Edit this occurrence" (reschedule, title, agenda) and
"Cancel this occurrence", shown on a recurring meeting for the
selected upcoming occurrence. Adds FR-046 so V2 ports them, narrows
FR-051's new-scope list to whole-series edit/delete and the O-01..O-04
dialogs, and adds the row to the section-visibility table and the
traceability table.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md, Counts: the "no count source" conclusion is scoped to
  upcoming meetings. On a past meeting any signed-in viewer with access,
  outsider included, has counts through the participants list; only
  anonymous viewers have none, matching FR-032.
- data-model.md, View-scoped state and Identifiers: ?occurrence= is a
  Unix-millisecond start timestamp, matched against start_time
  (meeting-join.component.ts:1022-1027), not an occurrence ID. The RSVP
  request takes the matched occurrence's seconds-based occurrence_id,
  never the URL value.

Also moves the spec directory from 010 to 011: main took 010 for
specs/010-shared-ghcr-cleanup (#3202).

Resolves 2 review threads.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2935 review feedback

Address review comments from the reviewers on PR #2935 (all per
copilot-pull-request-reviewer[bot]):

- data-model.md, state-matrix.md: cadence is `!!meeting.recurrence`
  for upcoming payloads, not `!== null` (an omitted field is not a
  series). For past meetings, whose payload can omit recurrence, it is
  derived from the series (getMeetingSeriesUid, as V1 does at TS:1047)
  or the occurrence timeline.
- requirements.md FR-050: passcode and dial-in leave the permanent
  exclusion list; they are E7 work held back until U-04 / U-05 (FR-043).
- requirements.md FR-063, checklist: browser redirect coverage is
  scoped to the Reachability section's illegal rows; impossible input
  combinations (Axis D on non-ended, RSVP states with tracking off) are
  covered by resolver-level unit tests.
- requirements.md traceability: adds FR-047 (occurrence data: widened
  and paginated endpoint, past attendance after U-01), FR-048 (derived
  recording duration) and FR-054 (per-meeting guard on the admin past
  route, own PR per R06). Adds rows for E0-01, E0-03, E4-02, E5-01
  (shipped), N-03, E6-01/02/06, E7-03, E8-01/02/03, E9-03 and the
  S-01/S-02 spikes. Checked against every plan-ID heading in the plan.

Resolves 4 review threads.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* docs(meetings): define the v2 meeting details testid contract

Adds specs/010-meeting-details-redesign/testid-contract.md: the
authoritative data-testid and data-* state list for the V2 meeting
details tree, written ahead of the components so every component PR and
the V2 E2E suite agree on the same selectors.

State lives in data-* attributes, never class names; dynamic testids key
on entity ids, never collection indexes. data-kind mirrors all nine
ActionSlotKind members rather than the six the issue body lists, which
predate the E0-02 viewer-state model. data-attendance is absent, not
"none", when invite responses are disabled. tools-summary-btn carries a
data-approval axis because V1 already renders that state as a badge.

No existing spec asserts on the V1 details tree: an exact-match search
for getByTestId across e2e/ finds no consumer among its 38 testids. The
two names that look like consumers are exercised on meeting-card via the
/meetings list, not on this page.

Points .claude/rules/development-rules.md at the contract so component
authors find it.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): correct the action-kind mapping in the testid contract

The note on the three kinds E0-02 added said one of them covers the
signed-in outsider on a restricted meeting. It does not: that state
resolves to invitation-required, one of the original six. The note now
states what each addition names as resolveActionSlot decides it:
guest-join, no-access (an ended meeting without artifact access) and
rsvp-unavailable.

Also closes three gaps in the same file:

- data-attendance and data-invitation had no value sets. They now
  mirror RegistrantAttendanceStatus and MeetingRegistrant.type.
- data-approval had no value for a summary that never required
  approval, which V1 renders with no badge. It is now not-required,
  not an absent attribute.
- V1 carries 56 distinct testids, not 38; the count missed the 18 bound
  through [attr.data-testid]. Re-verified that none is consumed by a
  spec visiting /meetings/:id.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): mark the spec.md cross-reference as pending #2914

The testid contract points at spec.md's silent-failure list, which lands with E0-01 (PR #2914) and is not on main yet. The reference now names the section and says where the file comes from, matching how this contract already marks its other unmerged siblings.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): align the testid contract with the signed-off join rules

State matrix decisions D-1 to D-3 (applied to the resolver in PR #2909) change two descriptions here: guest-join now covers every anonymous visitor in the join window, whatever the privacy, with the server matching the email on restricted meetings; and invitation-required is the restricted-meeting outsider only, since an outsider holding the link to a private unrestricted meeting can join.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): bring the testid contract up to date with main

main's V1 page gained organizer occurrence controls ("Edit this
occurrence", "Cancel this occurrence", #3040 and #3206). V2 must port
them, so the contract names their V2 testids:
meeting-occurrence-edit-button and meeting-occurrence-cancel-button.

The V1 inventory is recounted against main: 55 distinct testids (37
static, 18 bound), not 56. Re-verified that none is consumed by a spec
visiting /meetings/:id; meeting-title joins the shared-name table, as
its only consumer is a meeting-card spec on /meetings.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2913 review feedback

Address review comments from the reviewers on PR #2913 (all per
copilot-pull-request-reviewer[bot]):

- testid-contract.md: the status pill and privacy chip expose runtime
  state but had no state attributes, contradicting Rule 1. Adds
  meeting-status-pill[data-state] (MeetingTimeState) plus
  [data-my-rsvp] (registrant with tracking on only), and
  meeting-privacy-chip[data-visibility] / [data-restricted], mirroring
  MeetingPrivacyState.
- testid-contract.md: people rows are split by data source. Upcoming
  rows (MeetingRegistrant) keep data-attendance / data-invitation;
  past rows (PastMeetingParticipant, no RSVP and no type) get
  data-attended / data-invited. The identity-tier attribute waits for
  N-02.

Also moves the spec directory from 010 to 011: main took 010 for
specs/010-shared-ghcr-cleanup (#3202). The development-rules pointer
follows.

Resolves 2 review threads.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2913 review feedback

Address review comments from the reviewers on PR #2913:

- testid-contract.md: meeting-status-pill[data-my-rsvp] is now present
  for any invited viewer with RSVP tracking on, including an organizer
  who is also invited. The view model resolves that viewer as the
  organizer role and the action slot still returns rsvp for them, as
  V1 lets an invited organizer set and see their own RSVP. The
  attribute keys on Meeting.invited, not on the role (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #1768

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* feat(shared): add meeting details v2 view-model resolvers

Adds the pure view-model layer the meeting details V2 components will be
built on (#2876). Nothing consumes it yet — V1 is untouched.

New in @lfx-one/shared:
- meeting-view-model.interface.ts — MeetingTimeState, MeetingViewerRole,
  ActionSlotKind and the resolver input/output shapes.
- meeting-view-model.utils.ts — resolveTimeState, resolveViewerRole,
  resolvePrivacy, resolveActionSlot, resolveVisibleSections. Every
  function is total and deterministic; none reads the clock.

meeting.utils.ts changes, all additive:
- MEETING_END_BUFFER_MS moves above its four consumers, which each
  inlined the same forty-minute literal. No literal remains.
- canJoinMeeting and hasMeetingEnded take an optional trailing
  `now = new Date()`, matching isOccurrencePast in the same file. No
  call site changes and every existing spec stays green; this is what
  lets resolveTimeState reuse the real join window deterministically.

The resolvers name the three states V1 renders as an empty rail:
invitation-required, no-access and rsvp-unavailable. RSVP is hard-gated
on the normalized is_invite_responses_enabled — resolveActionSlot can
never return 'rsvp' and resolveVisibleSections zeroes every RSVP surface
when it is false.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(shared): align meeting V2 section and rail access rules

Addresses post-commit review findings on the meeting details V2 view
model. `resolveEndedActionSlot` gave an organizer `tools`
unconditionally while `resolveVisibleSections` gated `tools` on
`past_meeting_full_access` alone, so an organizer without that flag
resolved to a tools rail pointing at sections the same view model had
hidden. Both now share one `hasArtifactAccess` rule, and the past
roster is gated with the rest of the past content rather than on
membership alone.

Also documents `resolveTimeState`'s nullish-occurrence contract — it
describes the occurrence it is handed, falling back to the series'
start_time — and pins it with a test, since the caller owns occurrence
selection.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(shared): name the real full-access field and close unknown privacy

Addresses the whole-branch review of the meeting details V2 view model.

The access-gate comments named `past_meeting_full_access`, which greps
to nothing. The field is `full_access` on `PublicPastMeetingResponse`,
held client-side as `pastMeetingFullAccess`; the comments now say so,
since they are the only mapping a V2 component author has.

`resolvePrivacy` also inherited the label and icon helpers' fallthrough
for a null `visibility`, producing "Public" under a globe while
`openToPublic` stayed false and the rail told the same viewer they
needed an invitation. An absent visibility now reads private across
every field, so the header and the rail cannot disagree.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(shared): document the compiler prelude for shared specs

A Vitest spec under `packages/shared/src/` whose import graph reaches an
Angular declaration — `meeting.utils.ts` pulls `HttpParams` from
`@angular/common/http` — throws at import time without
`import '@angular/compiler';` first. Five specs already carry that
prelude as folklore; the package architecture doc now states the rule
next to the related barrel-import rule.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* test(shared): pin every action-slot cell in an explicit matrix

Replaces the totality sweep in the resolveActionSlot spec with a table
that writes out the expected kind for all 48 (time, viewer, privacy)
tuples across the four (fullAccess, inviteResponsesEnabled) pairs. The
sweep only asserted that each result was some valid ActionSlotKind, so
an unlisted cell could change to another kind and still pass, and it
never exercised private + restricted at all.

Two guard tests keep the table honest: every tuple appears exactly
once, and every ActionSlotKind is reachable. Proven binding per R07 by
returning 'none' for a live outsider on a private restricted meeting;
exactly that row failed.

Also adds the missing why-comment to the compiler prelude in
meeting-calendar.utils.spec.ts, which the package-architecture rule
this PR introduces requires.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(shared): let anyone holding the link join an unrestricted meeting

resolveActionSlot keyed joining on openToPublic (public AND
unrestricted), but in the app only self-registration needs that.
Reaching a non-open page already required the meeting password, so a
loaded page means the viewer holds the link, and an unrestricted
meeting lets anyone with the link join. Against V1 on main that
removed three things V1 allows (state matrix D-1 to D-3, signed off):

- D-1: every anonymous visitor in the join window now gets guest-join,
  whatever the privacy. On a restricted meeting the join-url endpoint
  matches the submitted email against the registrants, and this is
  how an invitee without an LFX session joins from their invite link.
  The resolver returned none and would have locked them out.
- D-2: a signed-in outsider on a live public-open meeting joins
  directly, as V1 lets them, instead of registering first.
- D-3: a signed-in outsider on a private unrestricted meeting joins
  when live and gets none before the window, instead of being told an
  invitation is required for a meeting that needs none.

openToPublic now gates registration only; the ActionSlotKind and
MeetingPrivacyState docs say so. D-4 (anonymous + ended public
meeting stays tools, rendered as sign-in until public artifact routes
exist) needed no resolver change.

The 48-row matrix is regenerated from the new rules and the named
cases updated. Proven binding per R07: restoring the old visitor rule
failed exactly the five cells it touches.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2909 review feedback

Address review comments from the reviewers on PR #2909:

- meeting-view-model.utils.ts, meeting-view-model.utils.spec.ts: stop
  presenting specs/010-meeting-details-redesign/state-matrix.md as an
  existing source of truth. It lands with E0-01 in PR #2935 and is not
  on main yet; both comments now name the spec's MATRIX table as the
  executable source and mark the written matrix as pending (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2909 review feedback

Address review comments from the reviewers on PR #2909:

- meeting-view-model.utils.ts: the RSVP surfaces (summary, roster
  filter, avatar badges) are roster chrome but keyed only on tracking
  and membership, so an ended registrant without artifact access got
  them while the same result hid the roster. They now also require
  contentVisible, like people. New test pins that cell; proven binding
  per R07 (dropping the gate failed exactly it) (per cursor[bot]
  Bugbot).
- meeting-view-model.interface.ts: the ActionSlotInput JSDoc pointed
  the viewer's own RSVP at MeetingSectionVisibilityInput, which has no
  RSVP field. It now names the RSVP card's own input (E2-04 / E2-05)
  (per copilot-pull-request-reviewer[bot]).

Resolves 2 review threads.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2909 review feedback

Address review comments from the reviewers on PR #2909:

- meeting-view-model.utils.ts: the past roster keyed on the viewer
  role, but the past endpoint never sets Meeting.invited (it folds
  registrant status into full_access in checkPastMeetingAccess), so a
  past registrant resolved as outsider and lost the roster meant for
  them. For an ended meeting, people now keys on "signed in with
  artifact access", matching v1, which loads past participants for any
  authenticated viewer with access; anonymous viewers still get none.
  New test pins it; proven binding per R07 (restoring the role-based
  rule failed exactly it).
- meeting-view-model.interface.ts: MeetingViewerContext.invited now
  documents that only the upcoming endpoint populates it (per
  copilot-pull-request-reviewer[bot]).

Resolves 1 review thread.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2909 review feedback

Address review comments from the reviewers on PR #2909 (all per
copilot-pull-request-reviewer[bot]):

- meeting-view-model.interface.ts: MeetingPrivacyState.visibility is
  MeetingVisibility, not MeetingVisibility | null. resolvePrivacy reads
  an absent visibility as private, so null is never produced, and the
  wider type let callers build a state the fail-closed resolver cannot.
- meeting-view-model.interface.ts: the viewer-role doc no longer claims
  the payload cannot distinguish a host. Host-key entitlement is
  orthogonal to the role and already on the payload
  (can_view_host_key / host_key, set for the FGA host relation,
  co-hosts included); read it directly, never via organizer.
- meeting-view-model.interface.ts: joinDetails covers the join link
  only. The host key is not gated by it, since a non-organizer co-host
  resolves as outsider.

Also points the spec-path comments at specs/011-meeting-details-redesign:
main took 010 for specs/010-shared-ghcr-cleanup (#3202).

Resolves 3 review threads.

Refs #2876

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* docs(meetings): add the v2 rollout and retirement plan

Adds specs/011-meeting-details-redesign/rollout.md for V2-03. It
separates the two controls: the integration branch decides whether the
code is in production at all (nothing reaches main until release PR
#3249), and MEETING_V2_ENABLED_FLAG decides who sees V2 once it is.

Defines eight stages (build, release, internal testers, extended
testers, signed-in percentage, anonymous, soak, retire), each with who
decides and exit criteria. States that the code default never changes
and targeting is the switch. Gives the anonymous plan: a code change
that needs #2920's server-side evaluation, then an anonymous
LaunchDarkly context ramped by percentage. Adds what is measured, a
rollback table, and flag retirement: this page drops its read of the
shared flag, and the flag itself retires only with the composer.

The V1-deletion checklist is filed as #3266. The README's contents
table now lists the files that actually exist.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): reconcile v2 spec cross-references after the merge

The foundation PRs (#2909, #2910, #2911, #2913, #2914, #2935) now sit
together on feat/meeting-details-v2, so the "pending sibling, not on
main yet" notes they carried are stale. They become plain
cross-references in spec.md, state-matrix.md, data-model.md,
requirements.md, testid-contract.md, v2-scaffold.md and the
meeting-view-model resolver comments.

Also records the newly filed Phase 1 issues (#3250-#3265) in spec.md's
work order and requirements.md's traceability table, and the V1
retirement issue #3266.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): fix rollout plan gating and rollback guidance

Make #2920 the stage 4 entry gate rather than a release blocker,
scope R05 and FR-001 to the stages before the anonymous stage, give
the V1 rollback a targeted-revert path, and stop reusing the D-2 label.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): address review feedback on the rollout plan

Make FR-001 platform-neutral with the signed-in and post-hydration
rules scoped to their stages, limit stage 2 to signed-in states, define
the baseline once a V1 cohort is gone, and re-wrap a spec paragraph.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
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.

2 participants