mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <github@multica.ai>
163 lines
4.1 KiB
Go
163 lines
4.1 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: activity.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countAssigneeChangesByActor = `-- name: CountAssigneeChangesByActor :many
|
|
SELECT
|
|
details->>'to_type' as assignee_type,
|
|
details->>'to_id' as assignee_id,
|
|
COUNT(*)::bigint as frequency
|
|
FROM activity_log
|
|
WHERE workspace_id = $1
|
|
AND actor_id = $2
|
|
AND actor_type = 'member'
|
|
AND action = 'assignee_changed'
|
|
AND details->>'to_type' IS NOT NULL
|
|
AND details->>'to_id' IS NOT NULL
|
|
GROUP BY details->>'to_type', details->>'to_id'
|
|
`
|
|
|
|
type CountAssigneeChangesByActorParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ActorID pgtype.UUID `json:"actor_id"`
|
|
}
|
|
|
|
type CountAssigneeChangesByActorRow struct {
|
|
AssigneeType interface{} `json:"assignee_type"`
|
|
AssigneeID interface{} `json:"assignee_id"`
|
|
Frequency int64 `json:"frequency"`
|
|
}
|
|
|
|
// Count how many times a user assigned each target via assignee_changed activities.
|
|
func (q *Queries) CountAssigneeChangesByActor(ctx context.Context, arg CountAssigneeChangesByActorParams) ([]CountAssigneeChangesByActorRow, error) {
|
|
rows, err := q.db.Query(ctx, countAssigneeChangesByActor, arg.WorkspaceID, arg.ActorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountAssigneeChangesByActorRow{}
|
|
for rows.Next() {
|
|
var i CountAssigneeChangesByActorRow
|
|
if err := rows.Scan(&i.AssigneeType, &i.AssigneeID, &i.Frequency); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const createActivity = `-- name: CreateActivity :one
|
|
INSERT INTO activity_log (
|
|
workspace_id, issue_id, actor_type, actor_id, action, details
|
|
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at
|
|
`
|
|
|
|
type CreateActivityParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
ActorType pgtype.Text `json:"actor_type"`
|
|
ActorID pgtype.UUID `json:"actor_id"`
|
|
Action string `json:"action"`
|
|
Details []byte `json:"details"`
|
|
}
|
|
|
|
func (q *Queries) CreateActivity(ctx context.Context, arg CreateActivityParams) (ActivityLog, error) {
|
|
row := q.db.QueryRow(ctx, createActivity,
|
|
arg.WorkspaceID,
|
|
arg.IssueID,
|
|
arg.ActorType,
|
|
arg.ActorID,
|
|
arg.Action,
|
|
arg.Details,
|
|
)
|
|
var i ActivityLog
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.ActorType,
|
|
&i.ActorID,
|
|
&i.Action,
|
|
&i.Details,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getActivity = `-- name: GetActivity :one
|
|
SELECT id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at FROM activity_log
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetActivity(ctx context.Context, id pgtype.UUID) (ActivityLog, error) {
|
|
row := q.db.QueryRow(ctx, getActivity, id)
|
|
var i ActivityLog
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.ActorType,
|
|
&i.ActorID,
|
|
&i.Action,
|
|
&i.Details,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listActivitiesForIssue = `-- name: ListActivitiesForIssue :many
|
|
SELECT id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at FROM activity_log
|
|
WHERE issue_id = $1
|
|
ORDER BY created_at ASC, id ASC
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListActivitiesForIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// All activities for an issue in chronological order, capped at $2 (DB safety
|
|
// net to bound the response).
|
|
func (q *Queries) ListActivitiesForIssue(ctx context.Context, arg ListActivitiesForIssueParams) ([]ActivityLog, error) {
|
|
rows, err := q.db.Query(ctx, listActivitiesForIssue, arg.IssueID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ActivityLog{}
|
|
for rows.Next() {
|
|
var i ActivityLog
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.ActorType,
|
|
&i.ActorID,
|
|
&i.Action,
|
|
&i.Details,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|