feat(cards): Phase ε — Pull-Requests + Card-Discussions

Server (cards-server):
- PullRequestService: create / list / get / merge / close / reject.
  Merge applies the PR's {add, modify, remove} diff to the latest
  version's cards in a single transaction, writes a new
  deck_version + deck_cards, bumps latest_version_id, and stamps
  the PR with mergedIntoVersionId.
- DiscussionService: post / listForCard / hide. Threads are keyed
  by card_content_hash so they survive version bumps.
- Routes mounted under /v1: POST/GET /decks/:slug/pull-requests,
  GET /pull-requests/:id, POST /pull-requests/:id/{merge,close,reject},
  GET/POST /cards/:contentHash/discussions, POST /discussions/:id/hide.

Frontend (cards-web):
- cardsApi.pullRequests + cardsApi.discussions client surface.
- <PullRequestsSection> on /d/:slug — lists PRs with diff preview;
  owner sees Merge/Reject/Close buttons.
- <SuggestEditModal> + "✏️ Verbessern" button on /learn/:deckId for
  cards from a subscribed deck — submits a one-card modify (or
  remove) PR using the card's serverContentHash as the previous
  hash.
- Deck/Card DTOs gain subscribedFromSlug + serverContentHash so the
  learn page can decide whether to show the suggest-edit affordance.
This commit is contained in:
Till JS 2026-05-07 21:56:20 +02:00
parent c84742005b
commit 61fc16e8e9
12 changed files with 1045 additions and 0 deletions

View file

@ -128,6 +128,10 @@ export interface Deck {
cardCount: number;
createdAt: string;
updatedAt: string;
/** Marketplace slug if this deck was pulled from a subscription. */
subscribedFromSlug?: string;
/** Semver of the subscribed-from version that's currently local. */
subscribedAtVersion?: string;
}
export interface Card {
@ -148,6 +152,13 @@ export interface Card {
nextReview?: string;
/** @deprecated legacy DTO field — read from cardReviews going forward. */
reviewCount?: number;
/**
* For cards from a subscribed deck: the server's content-hash for
* the card as it was published. The PR-creation flow uses this as
* `previousContentHash` when proposing a "modify" diff.
*/
serverContentHash?: string;
}
export interface CardReview {