fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m39s
CI / Deploy production (on server) (push) Successful in 22s

1. replaceAll (Find & Replace) — une seule transaction ProseMirror
   au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés.

2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs
   qui laissaient un nœud fantôme invisible dans le document.

3. Conversion Markdown → richtext — breaks: true dans marked.parse()
   Les simple newlines sont maintenant convertis en <br>.
   + préserve les blocs custom (toggle, callout, math, columns,
   outline, link-preview) en commentaires HTML lors de l'export MD.

4. emitNoteChange exercices — shape corrigée (type:'created' attend
   un objet Note, pas noteId/notebookId séparés).

5. Raccourcis clavier sans conflit :
   Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier)
   Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets)
   Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
Antigravity
2026-06-20 15:48:18 +00:00
parent 5b13a88b72
commit ee70e74bf5
51 changed files with 1483 additions and 252 deletions

View File

@@ -0,0 +1,163 @@
---
title: 'Subscription & Quota Admin Management'
type: 'feature'
created: '2026-06-20'
status: 'done'
baseline_commit: '5b13a88b726c03ca6ff5603d901147448a72adbc'
context:
- 'memento-note/lib/entitlements.ts'
- 'memento-note/lib/config.ts'
- 'memento-note/lib/billing/stripe-prices.ts'
---
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
## Intent
**Problem:** Subscription tiers, AI quotas, and Stripe business config are split between hardcoded code (`TIER_LIMITS`), server `.env` (price IDs, billing flag), and a minimal admin override (tier dropdown only). Operators cannot adjust limits or billing settings without redeploy; quota enforcement has a TOCTOU race (`check` then `increment`); token analytics are partially broken.
**Approach:** Add a secure admin **Billing & Quotas** console backed by DB (`PlanEntitlement` + `SystemConfig`), keep Stripe secrets in infra env only, unify quota enforcement on atomic `reserveUsageOrThrow`, and expose usage dashboards with audit logging for manual tier overrides.
## Boundaries & Constraints
**Always:**
- `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` stay in server env / secrets vault — never in `SystemConfig`, never editable from browser.
- Stripe remains authoritative for paid subscriptions; webhooks drive state; manual admin tier changes are support overrides only.
- Quota gating unit stays **requests per feature per calendar month** (not token-based blocking).
- Redis quota checks fail-open with structured error logging (industry standard for AI rate limits).
- All admin mutations require `role === ADMIN`; i18n for new UI strings in `locales/en.json` and `locales/fr.json` minimum.
- Non-destructive DB migration only; seed `PlanEntitlement` from current `TIER_LIMITS` defaults.
**Ask First:**
- Adding new Prisma models beyond `PlanEntitlement` (e.g. full `FeatureFlag` wiring).
- Changing Stripe products/prices in live Stripe Dashboard (ops action, not code).
- Switching to usage-based Stripe Meter / Metronome billing.
**Never:**
- Store `sk_*` or `whsec_*` in plaintext DB or admin forms.
- Run `prisma migrate reset`, `db push --accept-data-loss`, or any destructive DB command.
- Replace Redis real-time counters with synchronous PostgreSQL writes on every AI request.
- Add automated tests unless explicitly requested later.
## I/O & Edge-Case Matrix
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|----------|--------------|---------------------------|----------------|
| Admin saves tier limit | ADMIN edits PRO `chat` limit 50→75 | `PlanEntitlement` upserted; next `getLimit()` returns 75; existing Redis counters unchanged until period rollover | 403 if non-admin; 400 if invalid feature or negative limit |
| Admin saves Stripe price ID | ADMIN sets `STRIPE_PRICE_PRO_MONTHLY` in SystemConfig | `resolvePriceId('PRO','month')` reads DB value; env used only as fallback | 400 if empty when billing enabled |
| User at quota | User on BASIC, 30/30 `semantic_search` used | `reserveUsageOrThrow` returns 402 `QUOTA_EXCEEDED` | Fail-open if Redis down (allow + log alert) |
| Concurrent AI requests | Two requests at limit1 | Only one succeeds; second gets 402 (atomic Lua) | N/A |
| Admin manual tier override | ADMIN sets user to BUSINESS | `Subscription` upserted; `AuditLog` entry with actor, target, old/new tier | Cannot override own tier (existing rule) |
| Billing disabled | `BILLING_ENABLED=false` in SystemConfig | `/settings/billing` shows disabled state; checkout buttons hidden | N/A |
| suggest-charts usage | Successful chart suggestion | `incrementUsageAsync` called once (not broken 4-arg call) | N/A |
</frozen-after-approval>
## Code Map
- `memento-note/prisma/schema.prisma` — add `PlanEntitlement` model (`tier`, `feature`, `limitValue`; unique on `[tier, feature]`; `null` limitValue = unlimited; omit row or sentinel = feature unavailable per tier)
- `memento-note/prisma/migrations/*` — additive migration + seed from current `TIER_LIMITS`
- `memento-note/lib/entitlements.ts` — load limits from DB with in-memory cache (TTL ~60s) and hardcoded fallback; export `getLimit()` async; keep fail-open on Redis errors
- `memento-note/lib/billing/stripe-prices.ts` — read price IDs via `getConfigValue()` with env fallback; remove sole dependence on `process.env`
- `memento-note/lib/config.ts` — add billing keys to `ENV_FALLBACKS` (`BILLING_ENABLED`, four `STRIPE_PRICE_*`)
- `memento-note/app/actions/admin-billing.ts` — server actions: `getPlanEntitlements`, `updatePlanEntitlement`, `updateBillingConfig`, `getUsageOverview`
- `memento-note/app/(admin)/admin/billing/page.tsx` — admin UI: tier limits matrix, billing config, usage summary table
- `memento-note/app/(admin)/admin/billing/billing-admin-form.tsx` — client form component
- `memento-note/components/admin-sidebar.tsx` — nav link `/admin/billing`
- `memento-note/app/actions/admin.ts` — log `SUBSCRIPTION_OVERRIDE` audit on `updateUserSubscription`
- `memento-note/lib/audit-log.ts` — extend `AuditAction` with `SUBSCRIPTION_OVERRIDE`, `BILLING_CONFIG_UPDATED`, `PLAN_ENTITLEMENT_UPDATED`
- `memento-note/components/settings/billing-plans.tsx` — read `billingEnabled` from `/api/billing/status` instead of build-time env only
- `memento-note/app/api/billing/status/route.ts` — include `billingEnabled` from SystemConfig
- `memento-note/app/api/ai/suggest-charts/route.ts` — fix usage tracking (use `incrementUsageAsync`, not broken `trackFeatureUsage` call)
- AI routes using `checkEntitlementOrThrow` + `incrementUsageAsync` — migrate to `reserveUsageOrThrow` only (remove duplicate increment on success); include chat route
## Tasks & Acceptance
**Execution:**
- [x] `prisma/schema.prisma` + migration — add `PlanEntitlement`, seed defaults from `TIER_LIMITS` — DB-backed limits without breaking existing users
- [x] `lib/entitlements.ts` — async limit loader with cache + fallback; update `canUseFeature`, `reserveUsageOrThrow`, `getUserQuotas` — single source of truth
- [x] `lib/billing/stripe-prices.ts` + `lib/config.ts` — SystemConfig-backed price IDs and `BILLING_ENABLED` — admin-editable business config
- [x] `app/actions/admin-billing.ts` — CRUD entitlements + billing config + usage overview query (join `UsageLog` + Redis snapshot) — secure admin API surface
- [x] `app/(admin)/admin/billing/*` + `components/admin-sidebar.tsx` — admin UI section — operator-facing management
- [x] `app/actions/admin.ts` + `lib/audit-log.ts` — audit trail for tier overrides and config changes — support accountability
- [x] AI routes (grep `checkEntitlementOrThrow`) + `app/api/chat/route.ts` — switch to `reserveUsageOrThrow`; drop redundant `incrementUsageAsync` on success — fix TOCTOU race
- [x] `app/api/ai/suggest-charts/route.ts` — fix broken usage call — restore quota accounting
- [x] `app/api/billing/status/route.ts` + `components/settings/billing-plans.tsx` — runtime billing flag — no redeploy to toggle billing UI
- [x] `locales/en.json` + `locales/fr.json` — i18n keys under `admin.billing.*` — FR/EN reference labels
**Acceptance Criteria:**
- Given an ADMIN on `/admin/billing`, when they change PRO `chat` limit and save, then a new entitlement check within 60s reflects the new limit without app restart.
- Given a BASIC user at quota, when two parallel AI requests arrive, then at most one succeeds and the other returns HTTP 402.
- Given `BILLING_ENABLED` is false in SystemConfig, when a user opens `/settings/billing`, then checkout/upgrade actions are hidden and a clear disabled message is shown.
- Given an ADMIN changes a user's tier from `/admin/users`, when the change succeeds, then an `AuditLog` row records actor, target user, old tier, and new tier.
- Given Stripe price IDs are set in SystemConfig (env unset), when checkout is initiated, then the correct Stripe price is used.
- Given Redis is unreachable, when an AI request is made, then the request is allowed (fail-open) and an error is logged server-side.
- Given `STRIPE_SECRET_KEY` remains env-only, when inspecting `SystemConfig` table, then no `sk_` or `whsec_` values exist.
## Design Notes
**PlanEntitlement limit encoding:** `limitValue: null` → unlimited; positive integer → monthly request cap; omit row (or explicit `-1` sentinel if needed) → feature not available on tier (maps to current `undefined` in `TIER_LIMITS`).
**Cache invalidation:** After admin saves entitlements, call a small `invalidateEntitlementCache()` so changes apply immediately without waiting for TTL.
**Quota migration pattern:** Replace `await checkEntitlementOrThrow(userId, feature)` + `incrementUsageAsync(...)` with `await reserveUsageOrThrow(userId, feature)` at request start. Do not increment again on success. For streaming chat, reserve before `streamText`; no post-finish increment.
**Usage dashboard:** Show per-tier aggregate from `UsageLog` (last synced month) plus optional live Redis read for current month top features — degraded state if sync cron stale is acceptable (show `syncedAt`).
## Verification
**Commands:**
- `cd memento-note && npm run test:unit -- tests/unit/entitlements.test.ts` — expected: pass after async limit loader changes
- `cd memento-note && npx tsc --noEmit` — expected: no type errors
**Manual checks:**
- ADMIN `/admin/billing`: edit a limit, save, verify `/api/usage/current` reflects new cap for a test user on that tier.
- Non-admin GET to admin-billing server actions returns unauthorized.
- Two rapid AI calls at quota1: only one succeeds.
## Spec Change Log
## Suggested Review Order
**Entitlements & DB limits**
- Central loader: DB `PlanEntitlement` with 60s cache and hardcoded fallback
[`plan-entitlements.ts:1`](../../memento-note/lib/plan-entitlements.ts#L1)
- Prisma model + seeded migration from former `TIER_LIMITS`
[`schema.prisma:814`](../../memento-note/prisma/schema.prisma#L814)
- Entitlements API now reads limits asynchronously from cache
[`entitlements.ts:1`](../../memento-note/lib/entitlements.ts#L1)
**Admin console**
- Server actions: entitlements CRUD, billing config, usage overview
[`admin-billing.ts:1`](../../memento-note/app/actions/admin-billing.ts#L1)
- Admin UI at `/admin/billing` with tier matrix + Stripe price IDs
[`billing-admin-client.tsx:1`](../../memento-note/app/(admin)/admin/billing/billing-admin-client.tsx#L1)
- Audit trail on manual tier override
[`admin.ts:142`](../../memento-note/app/actions/admin.ts#L142)
**Stripe & billing UX**
- Price IDs and `BILLING_ENABLED` via SystemConfig (env fallback)
[`stripe-prices.ts:79`](../../memento-note/lib/billing/stripe-prices.ts#L79)
- Runtime billing flag exposed to settings page
[`billing-plans.tsx:80`](../../memento-note/components/settings/billing-plans.tsx#L80)
**Quota hardening**
- Atomic `reserveUsageOrThrow` on billable AI routes (replaces check+increment)
[`chat/route.ts:69`](../../memento-note/app/api/chat/route.ts#L69)
- Fixed broken suggest-charts usage tracking
[`suggest-charts/route.ts:58`](../../memento-note/app/api/ai/suggest-charts/route.ts#L58)
**Tests**
- Entitlements + billing price map unit tests updated
[`entitlements.test.ts:1`](../../memento-note/tests/unit/entitlements.test.ts#L1)