You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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>
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.
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.
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>
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>
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>
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.
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.
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.
Do not offer protected artifact tools to anonymous visitors
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.
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>
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.
meeting-view-model.utils.ts, .spec.ts: the comments no longer present state-matrix.md as existing on this branch. They name the spec's MATRIX table as the executable source and mark the written matrix as pending in docs(meetings): add the meeting details v2 state matrix and requirements #2935 (per copilot-pull-request-reviewer[bot]).
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>
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>
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>
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>
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.
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.
…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>
* 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>
* 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>
…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>
* 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>
* 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>
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (
nowis 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.tsis byte-identical, and no app or server code changes in this PR.What's here
interfaces/meeting-view-model.interface.tsMeetingTimeState,MeetingViewerRole,ActionSlotKind(9 kinds),MeetingPrivacyState,MeetingViewerContext,ActionSlotInput,MeetingSectionVisibilityInput,MeetingSectionVisibilityutils/meeting-view-model.utils.tsresolveTimeState,resolveViewerRole,resolvePrivacy,resolveActionSlot,resolveVisibleSectionsutils/meeting-view-model.utils.spec.tsutils/meeting.utils.tsMEETING_END_BUFFER_MSabove its four consumers;canJoinMeetingandhasMeetingEndedtake an optional trailingnow = new Date()so the resolvers can be tested without faking timedocs/architecture/shared/package-architecture.mdimport '@angular/compiler';prelude rule that shared specs reaching Angular declarations depend oninterfaces/index.ts,utils/index.tsDecisions worth a reviewer's attention
The RSVP hard gate is enforced in two places on purpose.
Meeting.is_invite_responses_enabledis only true for meetings created after the January 2024 invite-responses release. When it isn't true,rsvpSummary,rsvpRosterFilterandrsvpAvatarBadgesare each false — the rule is "no RSVP UI anywhere", not "no RSVP card" — andresolveActionSlotcan never returnrsvp.hasArtifactAccessis shared between the rail and the section resolvers. When they each carried their own rule, an organizer withoutPublicPastMeetingResponse.full_accessresolved to atoolsrail pointing at sections the same view model had just hidden. One cross-resolver test pins the cell where they previously disagreed.An absent
visibilityreads as private.Meeting.visibilityis 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.resolveTimeStatedescribes the occurrence it is handed, not the series. Both delegates fall back to the series'start_timewhen the occurrence is nullish, so a long-running series whose first occurrence is months past would resolve toendedwhile 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
ActionSlotKindmembers exist to name V1's silent failures.invitation-required(signed-in outsider on a restricted meeting) andrsvp-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
hostviewer 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_MSstays inmeeting.utils.tsrather than moving toconstants/. It was raised in review as an optional follow-up and judged not worth expanding this commit's scope for.Testing
git diff.yarn check-types,yarn format,yarn lint(0 errors) andyarn buildall clean../check-headers.shpasses.Refs #2876
🤖 Generated with Claude Code