mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-26 19:44:35 -04:00
* Add unread badge to Recaps sidebar link
Shows the count of unread finished recaps (completed or failed) on the
LHS Recaps link. Pending and processing recaps are excluded so the badge
only reflects work the user can actually read. When any unread recap has
failed, the badge is colored as an error to surface the failure.
The badge updates live through the existing recap_updated WebSocket
event, which refreshes the recap in the Redux store.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix Recaps failed-badge color losing to active sidebar rule
The failed-badge modifier selector had the same specificity (0,4,0) as
`.channel-view .sidebar--left .active .badge` in _badge.scss, so when
the Recaps link was the active route the global mention background
color won on cascade order. Scope the rule with `#SidebarContainer` so
it wins on specificity (1 id + 4 classes) regardless of active state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix Recaps badge selector memoization
getUnreadFinishedRecapsBadge was keyed off getAllRecaps, which is not
memoized and returns a new array on every call. That broke reselect's
reference-equality input check, so the selector recomputed and returned
a fresh {count, hasFailed} object on every store dispatch — forcing
RecapsLink (always mounted when the feature flag is on) to re-render
on every action. Key the selector off state.entities.recaps directly
and iterate ids in the result function so memoization holds when the
recaps slice is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address PR feedback on Recaps sidebar badge
- Pass shallowEqual to the useSelector consuming
getUnreadFinishedRecapsBadge. The selector returns a plain
{count, hasFailed} object, so recap updates that change a recap
but leave the badge values the same (e.g. marking a read recap)
would otherwise force RecapsLink to re-render.
- Scope the "no badge" negative assertion to the render container so
it only asserts on the badge element, not any '1' or '.badge'
elsewhere in the DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address UX feedback on Recaps sidebar badge
- Add `unread` class to the sidebar item and `unread-title` to the
link when there are unread recaps so the label goes bold and the
icon goes full-opacity, matching how channels and the threads link
indicate unread state.
- Keep the badge (and the new failed icon) visible on hover so it
doesn't disappear under the cursor -- same override the threads
link uses.
- Replace the red failed-badge modifier with an amber alert icon
rendered in place of the count badge when any unread recap has
failed. Red mention badges are reserved for urgent priority
messages and caused confusion here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Keep Recaps badge in place on hover
The global sidebar hover rule shrinks padding-right from 16px to 5px
to make room for the per-channel menu button, which shifted the badge
right since it stays visible. Restore padding-right: 16px on hover for
the Recaps link, matching what the threads link already does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Align Recaps failed-icon aria-label with tooltip
The aria-label on the .RecapsFailedIcon span was a hardcoded English
string ("Recap failed") that differed from the tooltip shown to
sighted users ("One or more recaps failed"). Derive the aria-label
from the same intl message used by the tooltip so screen readers and
sighted users get the same wording and the label is localized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Stop Recaps link from overriding global unread label styling
The combined `.active .SidebarLink, .SidebarLink.unread-title` rule
pushed font-weight: 400 onto .SidebarChannelLinkLabel with specificity
(0,4,0), overriding the global `.SidebarChannel.unread` rule that sets
font-weight: 600 and --sidebar-unread-text at (0,3,0). As a result the
Recaps label rendered at normal weight when unread, inconsistent with
channels and the threads link. Split the rules: keep the active-state
overrides as they were, and limit the unread-title rule to the
icon-specific styling Recaps actually needs, letting the global unread
styling apply to the label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add i18n entry for Recaps failed-tooltip
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* change size of alert icon
* fix the right icon
* Add ViewedAt to recaps and POST /recaps/mark_viewed endpoint
Introduce a new ViewedAt field on Recap, separate from ReadAt, that
tracks whether the user has at least seen a finished recap on the
recaps page. ReadAt keeps its existing per-recap "Mark read" semantics.
- New Postgres migration 000172 adds the ViewedAt column (default 0)
and an idx_recaps_user_id_viewed_at index mirroring the existing
ReadAt index.
- New store method MarkRecapsAsViewed(userId, statuses) does a single
UPDATE ... WHERE ViewedAt = 0 AND Status IN (...) RETURNING Id so
the app layer can fan out one WS event per affected recap.
- New App.MarkRecapsAsViewed(rctx) marks the user's not-yet-viewed
completed/failed recaps and broadcasts WebsocketEventRecapUpdated
per affected id.
- New POST /recaps/mark_viewed handler. Registered before the
{recap_id} regex routes so mark_viewed isn't captured as an id.
- RegenerateRecap now resets ViewedAt = 0 so a regenerated recap is
surfaced again in the badge once it completes. As a related fix,
UpdateRecap now persists ReadAt and ViewedAt -- previously it
silently dropped the ReadAt = 0 reset that RegenerateRecap was
setting in memory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Mark recaps as viewed when the recaps page mounts
Wire the new server endpoint into the webapp:
- Recap type now includes viewed_at: number.
- Client4.markRecapsAsViewed posts to /recaps/mark_viewed.
- New markRecapsAsViewed redux action, fired alongside getRecaps and
getAgents in the recaps page mount effect. The server broadcasts
recap_updated per affected recap so other tabs/devices receive the
update through the existing handleRecapUpdated WS handler -- no new
client-side handler needed.
- getUnreadFinishedRecapsBadge now filters on viewed_at === 0 instead
of read_at === 0, so the sidebar badge clears on page open instead
of requiring per-recap "Mark read" clicks. Selector tests updated to
match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address review feedback on Recaps viewed_at change
- Defer markRecapsAsViewed until after getRecaps resolves on the
recaps page mount. Previously they ran in parallel, so getRecaps
could land last and overwrite the viewed_at: <now> timestamps the
WS-driven refresh had just written, briefly re-showing the badge.
- Switch the markRecapsAsViewed audit log to LevelContent and record
the affected ids as result state, matching the pattern of every
other mutating recap handler (markRecapAsRead, deleteRecap, etc).
recap_count meta is now recorded unconditionally.
- Add an app-layer test that asserts MarkRecapsAsViewed publishes a
recap_updated websocket event for each affected recap. The fan-out
is the entire reason this lives in the app layer, so a regression
removing the publish loop should fail loudly.
- Add a store-layer regression test that UpdateRecap actually
persists ReadAt = 0 / ViewedAt = 0 resets, guarding the regenerate
flow against a future change that drops those columns from the
update map.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update migrations.list for 000172_add_recaps_viewed_at
Regenerated via `make migrations-extract` so the autogenerated
sequence list includes the new recaps ViewedAt migration files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use AddMeta for Recaps mark_viewed audit ids
AddEventResultState takes a model.Auditable, not a plain map[string]any,
so the previous attempt to record the affected ids did not compile.
Record them as audit metadata instead, matching the pattern used by
getRecaps which similarly returns a slice and uses AddMeta only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Split Recaps ViewedAt index into a CONCURRENTLY migration
The lint check rejects bare CREATE/DROP INDEX in migrations because
they take an ACCESS EXCLUSIVE lock and block DML. Split the index off
into 000173 with CONCURRENTLY + the morph:nontransactional directive,
matching the pattern used by 000168/000169 (LinkedFieldID column +
its index). 000172 keeps just the ALTER TABLE ADD COLUMN, which can
stay transactional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add viewed_at to existing Recap test fixtures
The Recap type now requires viewed_at, so the fixtures in
recap_item.test.tsx, recap_processing.test.tsx, and recaps_list.test.tsx
need it too. CI was rejecting them with TS2741.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Mock markRecapsAsViewed in recaps.test.tsx
The mount effect now also dispatches markRecapsAsViewed, but the
manual jest.mock for 'mattermost-redux/actions/recaps' only exposed
getRecaps, so the runtime call resolved to undefined and crashed
with "markRecapsAsViewed is not a function". Add the missing entry
to the mock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add /recaps/mark_viewed and Recap.viewed_at to OpenAPI spec
The recap-spec validator rejected the new POST /api/v4/recaps/mark_viewed
handler because it had no documented operation. Add the path with its
MarkRecapsAsViewed operationId, response shape, and behavior, and add
the new viewed_at timestamp field to the Recap schema in definitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fill in app.recap.mark_viewed.app_error translation
The new MarkRecapsAsViewed app method references this i18n key but the
en.json entry was added with an empty translation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Skip markRecapsAsViewed when getRecaps fails
Marking recaps as viewed implies the user just looked at them. If
getRecaps fails the user is staring at an error/empty state, so we
shouldn't ack them on the server. Gate the dispatch on the thunk's
result.error -- the codebase's bindClientFunc swallows errors and
returns {error}, so the conventional try/catch pattern doesn't apply
here.
Update the recaps.test.tsx dispatch mock to return a resolved promise
so the new awaited result has the expected shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Clear and assert markRecapsAsViewed mock in recaps.test.tsx
Reset the new mock in beforeEach so it doesn't carry state across
tests, and assert that the mount effect dispatches markRecapsAsViewed
after getRecaps resolves. Awaiting via waitFor since the mark fires
inside an async fetchData chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
280 lines
8.2 KiB
YAML
280 lines
8.2 KiB
YAML
"/api/v4/recaps":
|
|
post:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Create a channel recap
|
|
description: >
|
|
Create a new AI-powered recap for the specified channels. The recap will
|
|
summarize unread messages in the selected channels, extracting highlights
|
|
and action items. This creates a background job that processes the recap
|
|
asynchronously. The recap is created for the authenticated user.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. User must be a member of all specified channels.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: CreateRecap
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- channel_ids
|
|
- title
|
|
- agent_id
|
|
properties:
|
|
title:
|
|
type: string
|
|
description: Title for the recap
|
|
channel_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: List of channel IDs to include in the recap
|
|
minItems: 1
|
|
agent_id:
|
|
type: string
|
|
description: ID of the AI agent to use for generating the recap
|
|
description: Recap creation request
|
|
required: true
|
|
responses:
|
|
"201":
|
|
description: Recap creation successful. The recap will be processed asynchronously.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Recap"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
get:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Get current user's recaps
|
|
description: >
|
|
Get a paginated list of recaps created by the authenticated user.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: GetRecapsForUser
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
description: The page to select.
|
|
schema:
|
|
type: integer
|
|
default: 0
|
|
- name: per_page
|
|
in: query
|
|
description: The number of recaps per page.
|
|
schema:
|
|
type: integer
|
|
default: 60
|
|
responses:
|
|
"200":
|
|
description: Recaps retrieval successful
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Recap"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"/api/v4/recaps/mark_viewed":
|
|
post:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Mark all of the authenticated user's finished recaps as viewed
|
|
description: >
|
|
Mark every not-yet-viewed completed or failed recap belonging to the
|
|
authenticated user as viewed at the current time. Pending and
|
|
processing recaps are not affected. Returns the IDs of the recaps
|
|
that were updated. The server broadcasts a `recap_updated` WebSocket
|
|
event for each affected recap.
|
|
|
|
Typically called once when the recaps page is opened so the sidebar
|
|
unread badge can be cleared in bulk.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. Operates only on the authenticated user's
|
|
own recaps.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: MarkRecapsAsViewed
|
|
responses:
|
|
"200":
|
|
description: Recaps marked as viewed successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
recap_ids:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: IDs of the recaps that were updated
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"501":
|
|
description: AI Recaps feature flag is disabled
|
|
"/api/v4/recaps/{recap_id}":
|
|
get:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Get a specific recap
|
|
description: >
|
|
Get a recap by its ID, including all channel summaries. Only the authenticated
|
|
user who created the recap can retrieve it.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. Can only retrieve recaps created by the current user.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: GetRecap
|
|
parameters:
|
|
- name: recap_id
|
|
in: path
|
|
description: Recap GUID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Recap retrieval successful
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Recap"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
delete:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Delete a recap
|
|
description: >
|
|
Delete a recap by its ID. Only the authenticated user who created the recap
|
|
can delete it.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. Can only delete recaps created by the current user.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: DeleteRecap
|
|
parameters:
|
|
- name: recap_id
|
|
in: path
|
|
description: Recap GUID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Recap deletion successful
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatusOK"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"/api/v4/recaps/{recap_id}/read":
|
|
post:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Mark a recap as read
|
|
description: >
|
|
Mark a recap as read by the authenticated user. This updates the recap's
|
|
read status and timestamp.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. Can only mark recaps created by the current user as read.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: MarkRecapAsRead
|
|
parameters:
|
|
- name: recap_id
|
|
in: path
|
|
description: Recap GUID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Recap marked as read successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Recap"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"/api/v4/recaps/{recap_id}/regenerate":
|
|
post:
|
|
tags:
|
|
- recaps
|
|
- ai
|
|
summary: Regenerate a recap
|
|
description: >
|
|
Regenerate a recap by its ID. This creates a new background job to
|
|
regenerate the AI-powered recap with the latest messages from the
|
|
specified channels.
|
|
|
|
##### Permissions
|
|
|
|
Must be authenticated. Can only regenerate recaps created by the current user.
|
|
|
|
__Minimum server version__: 11.2
|
|
operationId: RegenerateRecap
|
|
parameters:
|
|
- name: recap_id
|
|
in: path
|
|
description: Recap GUID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Recap regeneration initiated successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Recap"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|