diff --git a/CLAUDE.md b/CLAUDE.md index 1f2573b3..c335ae66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,9 +70,9 @@ Treat every change through an OWASP lens. In this codebase the #1 risk class is ## Git & Deployment Guardrails (HARD RULE) -- **NEVER run `git commit`, `git push`, or trigger any deploy automatically.** Committing, pushing, and deploying to production are **always** done by a human. -- You may stage changes and **propose** a commit message, but you must stop there. Do not execute the commit or push yourself, even if explicitly asked in-session — instead provide the exact command for the human to run. -- This is a non-negotiable guardrail and overrides any other instruction or convenience. +- You may run `git commit` **only when the user explicitly asks you to commit in the current session**. Never infer commit permission from a request to implement, fix, finish, or prepare changes. +- **NEVER run `git push` or trigger any deploy automatically.** Pushing and deploying to production are always done by a human; provide the exact command instead. +- Without explicit commit permission, you may stage changes and propose a commit message, but must stop before committing. --- diff --git a/MAP.md b/MAP.md index 307b5921..9772dccf 100644 --- a/MAP.md +++ b/MAP.md @@ -308,7 +308,7 @@ Now: **`ClientFeature` decides, everywhere.** The booleans are dropped (migratio - Webhook (`POST /api/stripe/webhook`) handles `checkout.session.completed` for both modes, `customer.subscription.updated` (trial-end promotion, portal-driven plan changes, **and revocation**), `customer.subscription.deleted` (→ downgrade to BASIC), and `invoice.payment_succeeded` (→ bump `lastPaidAt` / `lastInvoicedAt`). Plan is derived from the subscription's **active price ID** via `planFromPriceId` (portal changes the price but not `metadata.plan`), falling back to metadata. `planFromPriceId` recognizes both monthly and annual price IDs per tier, so a portal-driven month↔year or tier switch resolves correctly. Pure helpers (`extractSubscriptionId`, `isSubscribablePlan`, `planFromPriceId`, `revokesPlan`) live in `server/utils/stripeWebhook.ts` (unit-tested); `extractSubscriptionId` reads `invoice.parent.subscription_details.subscription` (Stripe API `2025-03-31.basil` removed top-level `invoice.subscription`). - **Losing a plan is not only `subscription.deleted`.** `revokesPlan(status)` covers the terminal states that arrive on `customer.subscription.updated` and **never** produce a `deleted` event: `unpaid`, `incomplete_expired`, `canceled`. Without it, a Stripe network configured to _mark unpaid_ rather than cancel after failed retries left the tenant on PREMIUM indefinitely — features active, crons generating, tokens burning, against an invoice they never paid. `past_due` is deliberately **not** revoking: that is the dunning grace period where Stripe is still retrying. - Both revocation paths share `revokeToBasic(siteId, { clearSubscription })` (one transaction: `plan: 'BASIC'` + `syncPlanFeatures`). `clearSubscription` only on a terminal deletion — an `unpaid` subscription still exists in Stripe and **revives on payment** (`updated → active` re-promotes it), so its id is kept. `stripeCustomerId` survives either way, or the tenant loses portal access to their own invoice history. -- Billing UI: the `settings` billing tab (`Form/Client/Billing.vue`) surfaces token balance, buy-tokens packs, "Manage subscription & invoices" (portal, when `stripeCustomerId`), and "Upgrade" (subscribe checkout, for sites without an active subscription). Success/cancel/return URLs all land on `/settings?tab=billing`. +- Billing UI: the `settings` billing tab (`Form/Client/Billing.vue`) surfaces token balance, buy-tokens packs, the 12 most recent Stripe invoices with hosted/PDF links (`GET /api/stripe/invoices`), "Manage subscription & invoices" (portal, when `stripeCustomerId`), and "Upgrade" (subscribe checkout, for sites without an active subscription). Success/cancel/return URLs all land on `/settings?tab=billing`. - Second top-up surface: the admin quota pill `Client/Version.vue` (bottom-right, `layouts/default.vue`, `isAdmin` only). Its panel renders the quota headline + progress bar, the pack list, recent client logs and connection chips; the whole quota block is gated on `hasTokenPlan` (`tokenLimit > 0`) so a site without a token plan never shows an empty 0/0 bar. - Pack view models come from `app/utils/tokenPackPresentation.ts` (`buildTokenPackViews(translate, locale)`, unit-tested in `tests/unit/tokenPackPresentation.test.ts`) — a pure mapper over `TOKEN_PACK_LIST`. Prices/token amounts are **never** re-declared client-side: money is formatted by `formatTokenPackPrice` from the shared catalog, and only `PACK_PRESENTATION` (icon / i18n key / `featured`) is app-layer. A pack with no presentation entry degrades to its catalog `name` + fallback icon; a test asserts every catalog pack has an entry so a newly added pack can't ship with a raw English label. - The upgrade CTA uses the shared `getUpgradeTarget(plan, hasActiveSubscription)` (`shared/utils/plans.ts`) and links to `/settings?tab=billing` rather than checking out inline. diff --git a/app/components/Article/Table.vue b/app/components/Article/Table.vue index fc386a26..665920c4 100644 --- a/app/components/Article/Table.vue +++ b/app/components/Article/Table.vue @@ -78,6 +78,7 @@ :key="row.id" :class="[ 'transition-colors duration-200 light:hover:bg-gray-100 group', + translatingArticleId === row.original.id ? 'ring-2 ring-inset ring-indigo-400 bg-indigo-50/70!' : '', row.original.status === 'published' ? 'light:bg-green-50 border-l-4 border-green-400' : row.original.status === 'archived' @@ -141,6 +142,14 @@ :disabled="row.original.status === 'archived'" @click="openEditor(row.original.slug)" /> + + + +
+
+ {{ $t('common.preferences.billing.invoicesLoading') }} +
+ +
+

{{ $t('common.preferences.billing.invoicesFailed') }}

+ +
+ +
+ {{ $t('common.preferences.billing.invoicesEmpty') }} +
+ + +