Commit Graph
3 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 3048dd6637 style(prettier): sweep formatting across the tree
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:05:40 +02:00
Marc-André BombeckandClaude Opus 4.7 6863ecb87d fix(api): rate-limit feedback endpoint + drop overly broad cache invalidation (H5/H6 code-review)
H5 — `POST /api/insights/feedback` had no rate limit. The unique
index `(userId, recommendationId, recommendationText)` blocks exact
replays but a malicious client could distort the daily aggregator's
bucket slice by varying recommendationId/text per request. Added
`checkRateLimit("insights-feedback:<user.id>", 60, 60 * 60 * 1000)`.
60/h is generous given a comprehensive insight rarely has >10 recs.

H6 — `<RecommendationFeedback>` invalidated the entire `["insights"]`
prefix on every thumbs click, which matches all 8 insights queries
(comprehensive + 7 per-status). None of them depend on the feedback
row. The 24h server cache absorbs the actual LLM round-trip but the
network thrash + stale-cache refetch was waste. Dropped the
invalidation; user-visible verdict state lives in localStorage +
local React state. v1.4.17's prompt-version ratchet will scope its
own invalidation when feedback feeds the next generation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:11:34 +02:00
Marc-André BombeckandClaude Opus 4.7 8255e3e49a feat(api): /api/insights/feedback persists thumbs with provider attribution
v1.4.16 phase B5e: ship the POST endpoint that backs the per-rec
thumbs-up / thumbs-down UI. Body is the four rec-attributes the
client just rendered (id, text-snapshot, severity, metric-source +
time-range) plus the boolean verdict; server-fills providerType +
promptVersion via `resolveFeedbackAttribution()` so the client cannot
poison the daily aggregator's slice by claiming a thumbs-down was
attributed to a rival provider.

Idempotent via the existing `withIdempotency()` wrapper — a retry
with the same Idempotency-Key replays the cached response with
`X-Idempotent-Replay: true`. The dedup unique key on the model
(userId, recommendationId, recommendationText) returns 409 when the
same rec-text is re-rated — that's by design, the aggregator wants
per-text signal not per-id signal so a regeneration that rewrites
the same id with new text re-rates cleanly.

The attribution helper prefers `chainProviderType` over `providerType`
in the latest insights.generate audit row so when the B5b multi-
provider chain falls through to a secondary, the feedback row
reflects the working provider not the configured one. Falls back to
"unknown" + the current PROMPT_VERSION constant when no audit row
exists yet — the aggregator buckets that separately.

Audit row: action `insights.recommendation.feedback`, details carry
the feedback id, severity, helpful, providerType, promptVersion.

Tests:
  - 7 unit tests for the Zod request schema (valid path, severity
    poisoning, time-range poisoning, empty fields, stringified bool,
    extra-field stripping)
  - 5 unit tests for `pickProviderType()` priority + fallback
  - 4 integration tests: 201 happy path with provider attribution,
    409 dedup, 422 validation, fallback to unknown when no audit row

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:30:06 +02:00