pierre-review 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { getActivity, getConsolidatedFeed, listClaudeReviewsByRepo } from '../../db/queries.js';
1
+ import { getActivity, getConsolidatedFeed, listClaudeReviewsByRepo, markFeedSeen, } from '../../db/queries.js';
2
2
  import { accountIdOf } from '../plugins/auth.js';
3
3
  function parseIntList(raw) {
4
4
  if (!raw)
@@ -35,6 +35,13 @@ export async function activityRoutes(app) {
35
35
  allowBotIds: parseIntList(q.allowBotIds),
36
36
  });
37
37
  });
38
+ // Mark the Activity Feed as seen (bumps the account's server-side "seen" marker to
39
+ // now). Called when the user views the feed; resets the "new FYI since last here"
40
+ // count that drives the Welcome-back banner. Account-scoped; no body.
41
+ app.post('/api/activity/feed/mark-seen', async (req) => {
42
+ const at = await markFeedSeen(accountIdOf(req));
43
+ return { feedLastSeenAt: at.toISOString() };
44
+ });
38
45
  // Repo-scoped Claude-review history for the Activity single-repo console. Ownership +
39
46
  // feature-gating are handled inside the query (an unowned repo / disabled feature
40
47
  // both return an empty list), so the caller never leaks another account's data.
@@ -2,7 +2,7 @@ import { config } from '../../config.js';
2
2
  import { accountToLocalUser } from '../../auth/account.js';
3
3
  import { accountIdOf } from '../plugins/auth.js';
4
4
  import { getProCapabilities } from '../../pro/contract.js';
5
- import { dismissMyTurn, getCompletedDismissals, getMyTurn, undismissMyTurn, } from '../../db/queries.js';
5
+ import { countNewMyTurnFeedItems, dismissMyTurn, getCompletedDismissals, getFeedLastSeenAt, getMyTurn, markFeedSeen, undismissMyTurn, } from '../../db/queries.js';
6
6
  const dismissSchema = {
7
7
  body: {
8
8
  type: 'object',
@@ -19,8 +19,16 @@ const dismissSchema = {
19
19
  };
20
20
  export async function meRoutes(app) {
21
21
  app.get('/api/me', async (req) => {
22
+ const accountId = accountIdOf(req);
22
23
  const user = accountToLocalUser(req.account);
23
- const myTurn = await getMyTurn(accountIdOf(req));
24
+ const myTurn = await getMyTurn(accountId);
25
+ // Feed "seen" marker + how many FYI items are new since. Only counted once a
26
+ // baseline exists (feedLastSeenAt set by the first feed view) so a fresh account
27
+ // never sees a scary first-load number.
28
+ const feedLastSeen = await getFeedLastSeenAt(accountId);
29
+ const newFeedItems = feedLastSeen
30
+ ? await countNewMyTurnFeedItems(accountId, feedLastSeen)
31
+ : 0;
24
32
  return {
25
33
  user,
26
34
  counts: {
@@ -31,6 +39,8 @@ export async function meRoutes(app) {
31
39
  watchedRepoPrs: myTurn.watchedRepoPrs.length,
32
40
  claudeReviewsToAction: myTurn.claudeReviewsToAction.length,
33
41
  },
42
+ feedLastSeenAt: feedLastSeen ? feedLastSeen.toISOString() : null,
43
+ newFeedItems,
34
44
  claudeReviewEnabled: config.claudeReviewEnabled,
35
45
  deploymentMode: config.deploymentMode,
36
46
  pro: getProCapabilities(),
@@ -1,9 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { getAccessToken, getAccountUserId } from '../../auth/account.js';
3
3
  import { fetchActionsJobLog } from '../../github/actions-logs.js';
4
- import { getMentionCandidates, getPrDetail, getPrFilesContext, getPrWriteContext, markAllViewed, markPrViewed, upsertLocalPrComment, upsertLocalReview, } from '../../db/queries.js';
4
+ import { getMentionCandidates, getPrDetail, getPrFilesContext, getPrWriteContext, getReviewerLogins, markAllViewed, markPrViewed, upsertLocalPrComment, upsertLocalReview, } from '../../db/queries.js';
5
5
  import { buildFileAnchors, fallbackAnchor, isFindingAnchored, } from '../../github/diff-anchor.js';
6
- import { addIssueComment, fetchHeadShaFor, fetchPrFilesWithPatch, postInlineComment, submitPrReview, } from '../../github/mutations.js';
6
+ import { addIssueComment, fetchHeadShaFor, fetchPrFilesWithPatch, postInlineComment, requestReviewers, rerunWorkflowRun, submitPrReview, } from '../../github/mutations.js';
7
7
  import { hydratePrDetail } from '../../sync/hydrate-detail.js';
8
8
  import { accountIdOf } from '../plugins/auth.js';
9
9
  // GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its
@@ -56,6 +56,18 @@ const markViewedSchema = {
56
56
  properties: { sha: { type: 'string' } },
57
57
  },
58
58
  };
59
+ const ciRerunSchema = {
60
+ ...idParamSchema,
61
+ body: {
62
+ type: 'object',
63
+ required: ['runId', 'mode'],
64
+ additionalProperties: false,
65
+ properties: {
66
+ runId: { type: 'integer', minimum: 1 },
67
+ mode: { type: 'string', enum: ['failed', 'all'] },
68
+ },
69
+ },
70
+ };
59
71
  const markAllViewedSchema = {
60
72
  body: {
61
73
  type: 'object',
@@ -94,6 +106,22 @@ const reviewCommentSchema = {
94
106
  },
95
107
  },
96
108
  };
109
+ const requestReviewersSchema = {
110
+ ...idParamSchema,
111
+ body: {
112
+ type: 'object',
113
+ required: ['userIds'],
114
+ additionalProperties: false,
115
+ properties: {
116
+ userIds: {
117
+ type: 'array',
118
+ minItems: 1,
119
+ maxItems: 15,
120
+ items: { type: 'integer' },
121
+ },
122
+ },
123
+ },
124
+ };
97
125
  export async function prRoutes(app) {
98
126
  // Bulk "mark all seen": stamp every open PR (optionally scoped to repoIds) viewed
99
127
  // at its head, clearing all new-since badges at once. Static path — no :id — so it
@@ -357,5 +385,92 @@ export async function prRoutes(app) {
357
385
  const result = await fetchActionsJobLog(token, ctx.owner, ctx.name, jobId, tail ?? 200);
358
386
  return result;
359
387
  });
388
+ // Re-trigger a GitHub Actions workflow run for this PR. The `runId` comes from
389
+ // CheckRun.runId (Actions checks only). Server re-checks write access (WRITE/
390
+ // MAINTAIN/ADMIN, matching viewerCanPush — no author exclusion, unlike approve),
391
+ // then queues the rerun via the per-account token (local + cloud). GitHub runs it
392
+ // asynchronously; the refreshed check states arrive on the next sync.
393
+ app.post('/api/prs/:id/ci/rerun', { schema: ciRerunSchema }, async (req, reply) => {
394
+ const { id } = req.params;
395
+ const { runId, mode } = req.body;
396
+ const accountId = accountIdOf(req);
397
+ const ctx = await getPrWriteContext(id, accountId);
398
+ if (!ctx) {
399
+ reply.status(404);
400
+ return { error: 'NotFound', message: `PR ${id} not found` };
401
+ }
402
+ const canRerun = ['WRITE', 'MAINTAIN', 'ADMIN'].includes(ctx.viewerPermission ?? '');
403
+ if (!canRerun) {
404
+ reply.status(403);
405
+ return {
406
+ error: 'NotPermitted',
407
+ message: 'You need write access to this repo to re-run CI.',
408
+ };
409
+ }
410
+ try {
411
+ const token = await getAccessToken(accountId);
412
+ await rerunWorkflowRun(token, ctx.owner, ctx.name, runId, mode);
413
+ const result = { status: 'queued', runId, mode };
414
+ return result;
415
+ }
416
+ catch (err) {
417
+ reply.status(502);
418
+ return {
419
+ error: 'GitHubError',
420
+ message: err instanceof Error ? err.message : String(err),
421
+ };
422
+ }
423
+ });
424
+ // Request reviewers on a PR (powers the Insights "Assign reviewers" action). Server
425
+ // re-checks write access (WRITE/MAINTAIN/ADMIN — push-style, no author exclusion:
426
+ // an author may request reviewers on their own PR). The given user ids are resolved
427
+ // to GitHub logins (the PR author + bots + unknown ids dropped); GitHub itself gates
428
+ // that each login is a repo collaborator. The refreshed request state arrives on the
429
+ // next sync (reviewRequests are re-derived each pass).
430
+ app.post('/api/prs/:id/request-reviewers', { schema: requestReviewersSchema }, async (req, reply) => {
431
+ const { id } = req.params;
432
+ const { userIds } = req.body;
433
+ const accountId = accountIdOf(req);
434
+ const ctx = await getPrWriteContext(id, accountId);
435
+ if (!ctx) {
436
+ reply.status(404);
437
+ return { error: 'NotFound', message: `PR ${id} not found` };
438
+ }
439
+ const canRequest = ['WRITE', 'MAINTAIN', 'ADMIN'].includes(ctx.viewerPermission ?? '');
440
+ if (!canRequest) {
441
+ reply.status(403);
442
+ return {
443
+ error: 'NotPermitted',
444
+ message: 'You need write access to this repo to request reviewers.',
445
+ };
446
+ }
447
+ // Drop the PR author (GitHub rejects self-review requests), then resolve to
448
+ // logins (also drops bots + unknown ids).
449
+ const wanted = userIds.filter((uid) => uid !== ctx.authorId);
450
+ const logins = (await getReviewerLogins(wanted)).map((r) => r.login);
451
+ if (logins.length === 0) {
452
+ reply.status(400);
453
+ return {
454
+ error: 'NoReviewers',
455
+ message: 'None of the selected users can be requested as reviewers.',
456
+ };
457
+ }
458
+ try {
459
+ const token = await getAccessToken(accountId);
460
+ await requestReviewers(token, ctx.owner, ctx.name, ctx.number, logins);
461
+ const result = {
462
+ status: 'ok',
463
+ requestedLogins: logins,
464
+ };
465
+ return result;
466
+ }
467
+ catch (err) {
468
+ reply.status(502);
469
+ return {
470
+ error: 'GitHubError',
471
+ message: err instanceof Error ? err.message : String(err),
472
+ };
473
+ }
474
+ });
360
475
  }
361
476
  //# sourceMappingURL=prs.js.map
package/dist/config.js CHANGED
@@ -99,6 +99,16 @@ export const config = {
99
99
  // this entirely (one always-on account). SYNC_ACTIVE_WINDOW_MINUTES overrides.
100
100
  syncActiveWindowMinutes: intFromEnv('SYNC_ACTIVE_WINDOW_MINUTES', 15),
101
101
  stallThresholdDays: intFromEnv('STALL_THRESHOLD_DAYS', 3),
102
+ // ---- Retention / TTL ----
103
+ // Per-account server data doesn't grow forever: a periodic sweep prunes PRs (and their
104
+ // whole subtree) whose `updatedAt` is older than this many days. 0 disables it. The
105
+ // effective cutoff is clamped to at least `backfillDays`, so a forced full sync (which
106
+ // re-walks `now − backfillDays`) can never re-fetch (resurrect) a just-deleted PR.
107
+ // Runs in BOTH modes (local + cloud). RETENTION_DAYS overrides.
108
+ retentionDays: intFromEnv('RETENTION_DAYS', 180),
109
+ // When the retention sweep runs (node-cron). Daily at 03:00 by default; off-peak so a
110
+ // large delete doesn't contend with the 5-minute sync. RETENTION_CRON overrides.
111
+ retentionCron: process.env.RETENTION_CRON ?? '0 3 * * *',
102
112
  // Disable the periodic scheduler (used by scripts/tests).
103
113
  disableScheduler: process.env.DISABLE_SCHEDULER === 'true',
104
114
  // ---- Cloud (Railway) — GitHub App OAuth + sessions + token encryption ----
@@ -0,0 +1,7 @@
1
+ -- Server-side Activity-Feed "seen" marker (additive). `feed_last_seen_at` records the
2
+ -- last time this account viewed the consolidated feed, so "new FYI since you were last
3
+ -- here" is server-truth (consistent across devices) instead of a client localStorage
4
+ -- heuristic — the successor to the removed per-item "Done". Nullable; existing rows stay
5
+ -- NULL (no baseline yet → nothing counts as new until the first feed view sets it).
6
+ -- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
7
+ ALTER TABLE `accounts` ADD `feed_last_seen_at` integer;
@@ -148,6 +148,13 @@
148
148
  "when": 1780800000011,
149
149
  "tag": "0020_claude_review_cost_telemetry",
150
150
  "breakpoints": true
151
+ },
152
+ {
153
+ "idx": 21,
154
+ "version": "6",
155
+ "when": 1780800000012,
156
+ "tag": "0021_account_feed_last_seen",
157
+ "breakpoints": true
151
158
  }
152
159
  ]
153
160
  }
@@ -0,0 +1 @@
1
+ ALTER TABLE "accounts" ADD COLUMN "feed_last_seen_at" timestamp with time zone;