Skip to content

MM-69273 - Page comments: threaded footer/inline comments, resolve, editing, move re-home, retention, audit - #40

Draft
catalintomai wants to merge 3 commits into
masterfrom
MM-69273-comments
Draft

MM-69273 - Page comments: threaded footer/inline comments, resolve, editing, move re-home, retention, audit#40
catalintomai wants to merge 3 commits into
masterfrom
MM-69273-comments

Conversation

@catalintomai

@catalintomai catalintomai commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds page comments to the Docs plugin: threaded footer and inline comments with resolve/unresolve, author-only message editing, a has-replies delete guard, keyset-paged listings with filters, ids-only WebSocket events, cross-space move re-home, granular-retention enrolment for space channels, and audit records on every mutating route. The server surface is this PR entire; the RHS Comments panel that consumes the API is client work in a later PR.

A comment is a core Posts row (Type: custom_page_comment) on the space's backing channel, carrying a page_id prop as its only provenance — reusing the platform's post lifecycle (authorship, soft-delete cascade, reply threading) instead of a parallel comment store, while the paired core PR keeps those rows out of every chat surface.

API

Seven routes under /spaces/{space_id}/pages/{page_id}/comments:

Route Behavior
POST /comments Create a footer comment, or an inline one with a required opaque anchor_id (≤128 runes); both inline↔anchor half-states are 400s
GET /comments Roots only, keyset cursor (after), resolved and comment_type filters composed in SQL
POST /comments/{id}/replies Reply; sub-replies rejected 400 before core is called; replies inherit the root's kind and anchor
GET /comments/{id}/replies Offset-paged replies
GET /comments/{id} Roots and replies, full cross-reference predicate (comment ∈ page ∈ space, else 404)
PATCH /comments/{id} resolved (roots only, any member, attribution both directions via resolved_by/resolved_at) and message (author-only, roots and replies, refused 403 whole for a non-author; edit_at on the payload)
DELETE /comments/{id} Author deletes own; a root with live replies is refused 409 with reply_count in the body; any other member force-deletes through and the platform cascade takes the replies

Every mutating route emits an audit record through LogAuditRec — fail-by-default, flipped to success after the write, with Auditable() result/prior state (comment text and page bodies never reach the log). A source-scan ratchet pins the 21-route count so a future mutating route cannot ship unaudited by omission.

Storage and concurrency

  • No plugin table and no migration: comments are core Posts rows, read through the plugin store with literal-key JSON predicates (Props->>'page_id') that match the paired core PR's partial index.
  • WithPageCommentLock — a FOR SHARE on the page row plus a per-thread advisory lock in one transaction — serializes comment writes against MovePageToSpace, so a comment lands in the space its caller was authorized against, or not at all. Commit-ambiguity probes decide error paths by re-reading committed state, and a committed-but-errored write still publishes its WebSocket event.
  • Cross-space page moves re-home every comment thread through the new core MovePostsToChannel primitive (source-channel-narrowed on the routine path, space-wide on the re-issue repair path, chunked, with a non-convergence guard). Stragglers from a failed re-home are detectable by their surviving page_id and repaired by re-issuing the move. Detection names live and soft-deleted roots only: a root's edit-history rows carry RootId='' with OriginalId set, so they are excluded as inputs and travel through the primitive's OriginalId leg instead — the primitive rejects a batch that names one directly.

Retention

A RetentionPolicyId plugin setting enrols every space backing channel into a granular data-retention policy — at space creation, and via a reconcile sweep on configuration change that re-homes channels off a previous policy (core's assignment is keyed on the channel alone and its add silently no-ops on conflict, so the sweep must remove-then-add). Without the setting, nothing is enrolled and comments follow the server's ordinary clocks.

Core/plugin boundary

Core owns the post rows and every invariant around them: the write paths and hook clamps, counters and thread metadata, caches, the post-id authorization gates, retention, and the move primitive. The plugin owns the comment product policy: which posts are comments, the API and its locking, projections (ChannelId is never serialized), WS events, re-home and enrolment timing. The plugin writes no core table — every core-owned mutation goes through the plugin API.

Pre-RBAC authorization posture: space membership is the gate on every route, matching the rest of the plugin; the per-permission split (comment_page, delete_page_comment) wires up when the MM-69269 RBAC branch merges.

Verified live against the paired stack

1-docs-home

Docs home on the paired stack.

2-space-home

A space and its page tree, served by the branch build.

3-published-page

A published page — the object every comment hangs off. Its thread lives in the API until the RHS panel lands; the comments seeded on this page are the ones the transcripts below exercise.

Testing

  • Go: ~90 comment-scenario tests across model/store/app/handler — the prop-preservation red test (a props-only UpdatePost orphans the comment), resolve attribution both directions, cursor semantics under mid-walk deletes and CreateAt ties, the delete guard and cascade, edit-history-row invisibility (the app harness emulates core's re-keyed soft-deleted history rows and conditional EditAt), move re-home with straggler repair, chunking and non-convergence, retention enrolment and policy re-home, audit record shapes, and a wire-level reply+edit round-trip. Full suite green against the pinned server/public; golangci-lint clean.
  • Core-side (in the paired PR): plugin-API post mutations on space channels, the move primitive with counters/thread metadata/history rows, the suppression and gate family, and the partial-index catalog pin.
  • Playwright E2E: three API-driven specs — the comment thread lifecycle (create both kinds, validation half-states, reply inheritance, resolve/unresolve attribution, author-only editing, guarded delete + cascade), the keyset cursor walk, and move-to-space re-home. They run in CI against the paired core image selected by the marker below, and locally against the docs-core dev server (./scripts/run-tests.sh e2e-ui-local).
  • Smoke: ./scripts/run-smoke.sh drives flag-off (501 gates over every route group, comments included), space/page CRUD, and a live comment suite against a real server built from the paired core branch — including the core-REST 403s on a comment's post id and a DB-asserted re-home.

Dependency

Requires mattermost/mattermost#38207 (MovePostsToChannel, retention assignment plugin APIs, backing-channel post writes and suppression). server/public is pinned to that PR's head commit; CI E2E selects the paired image through the marker below.

Ticket Link

Fixes: https://mattermost.atlassian.net/browse/MM-69273

Release Note

Added threaded page comments: footer and inline comments with replies, resolve/reopen, author editing, filtered listings, move-aware re-homing, retention policy enrolment, and audit logging.

…e, editing, move re-home, retention, and audit

A page comment is a core Posts row (Type custom_page_comment) on the
space's backing channel, carrying a page_id prop as its only provenance.

- Seven routes under /spaces/{space_id}/pages/{page_id}/comments:
  footer and inline creates with anchor validation, keyset-paged roots
  listing with resolved/comment_type filters, offset-paged replies,
  resolve/unresolve with last-state-change attribution both directions,
  author-only message editing (edit_at on the payload), and a delete
  guarded 409 by live-reply count with member force-through.
- WithPageCommentLock (page FOR SHARE + per-thread advisory lock)
  serializes comment writes against page moves; commit-ambiguity probes
  decide error paths by re-read, and committed writes always publish
  their ids-only page_comment_* events.
- Cross-space page moves re-home comment threads through the core
  MovePostsToChannel primitive, chunked, with straggler detection keyed
  on the surviving page_id and a non-convergence guard. Edit-history
  rows are excluded from the misplaced-roots detection: they ride the
  primitive's OriginalId leg and are rejected as direct input.
- A RetentionPolicyId setting enrols space backing channels into a
  granular retention policy at creation, with a reconcile sweep that
  re-homes channels off a previous policy by remove-then-add.
- Every mutating route emits an audit record (fail-by-default, success
  after the write, Auditable() payloads that exclude user content),
  with a source-scan ratchet pinning the mutating-route count.
- API-driven Playwright specs (comment lifecycle, cursor walk, move
  re-home) with a comment-RPC preflight; CI resolves the paired core
  image from the e2e-core-commit PR marker.
- server/public pinned to the paired core branch head.
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.

1 participant