Port A-group deltas from design zip (excluding bonus/pricing which
are explicitly out of scope):
- Narrative: N (not 5) 个订阅; add '// 5 分钟开始用' n-kicker;
SVG hexagon logo (was emoji); n-bottom live status bar
- Add 3-step onboarding panel (创建账户 → 绑定订阅 → 生成 key)
in narrative, active-step highlighted
- Add password strength meter (4 bars + text label 弱/中/强/极强)
- Add confirm-password field with live // matched/mismatch hint
- Add Terms & Privacy consent checkbox (submit gated)
- New i18n keys: confirmPasswordLabel/Placeholder, passwordsDoNotMatch
All existing Vue logic preserved (OAuth/Turnstile/verify code/
invitation+promo codes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three regressions from Task 7-9 caused /login /register to render broken:
- bg-glow not rendering: puro.css scopes .bg-glow to .puro-page,
AuthLayout isn't inside one. Fix: duplicate bg-glow rules into
AuthLayout scoped CSS keyed on .auth-shell-split.
- .auth-main had no background: right side showed naked body bg.
Fix: .auth-shell-split now sets var(--bg-0) for whole shell.
- Heading/label colors used text-gray-900 light-mode classes,
invisible on dark bg. Fix: switch to explicit text-slate-50/400,
and :deep() override for form inputs via AuthLayout split scope.
Legacy (non-split) mode unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Login and Register heading strings moved from hardcoded Chinese to
auth.puroLoginTitle / puroLoginSub / puroRegisterTitle / puroRegisterSub.
Landing (LandingView) and Docs (DocsView) intentionally keep hardcoded
Chinese this cycle (see spec §6 note 5 — English version deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route /docs (no auth). Six sections: get key, codex CLI, claude code,
curl, supported models, feedback. Uses puro.css design system.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same split layout as LoginView: left narrative, right form.
Heading: '创建账户' + '5 分钟开始用 PURO AI'.
All form logic preserved (OAuth, Turnstile, email verify code, password fields).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Left: ⬢ PURO AI brand + '5→1' headline + three-line value props
- Right: existing form (OAuth, Turnstile, 2FA all preserved unchanged)
- Heading changed from t('auth.welcomeBack') to '登录' — i18n key consolidation in Task 11
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New slot 'narrative' enables split-screen layout (50/50 desktop, collapses
to single column on mobile <900px).
Backward compatibility:
- Pages that don't pass a narrative slot still render the original
centered-card layout with siteName + logo + copyright
- ForgotPassword, ResetPassword, EmailVerify unaffected
To be used in Tasks 8 and 9 (LoginView, RegisterView).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add noreferrer to all external rel attributes (4 anchors)
- Change 更新日志 link from /releases (may 404) to /commits/branch/main
- Remove dead CSS overridden by puro.css: .nav z-index:10, .nav-links gap:20px
- Document puro.css global dependency at top of scoped style block
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 4-column footer (brand/产品/资源/联系), responsive 2-col on mobile
- Nav sticky with blur background, border-bottom
- nav-links hidden on mobile (<640px)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dashboard mockup is pure static (SVG chart + stats grid + log table).
No backend dependency. Reuses puro.css .log-table / .provider / .status-*
(scoped to .puro-page). Only adds component-local styles for .code-demo,
.dash-mock, .stat-row, .chart-card and friends.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Temporary route /landing-preview added for dev iteration. Will flip /
to this view once all 6 sections are in place (Task 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One missed prefix from the automated transform. Aligns with the
scoping contract established in 41664efe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add puro.css (tokens + primitives) as global stylesheet
- Load Inter + JetBrains Mono via Google Fonts
- Extend tailwind.config with puro.* color namespace (no conflict with legacy primary/accent palettes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add quota exceeded check to IsSchedulable() and refactor
shouldClearStickySession to delegate to IsSchedulable(), eliminating
duplicated logic and fixing missed overload/rate-limit/expired checks.
Frontend displays quota exceeded status independently via quota fields.
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.
Treat those fields as write-only from the admin surface:
- decryptAndMaskConfig() strips sensitive keys from the GET response.
The authoritative list is an explicit per-provider registry that
mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
alipay → privateKey, publicKey, alipayPublicKey
wxpay → privateKey, apiV3Key, publicKey
stripe → secretKey, webhookSecret (publishableKey stays plain)
easypay → pkey
Payment runtime still reads the full config via decryptConfig, so
nothing at the gateway changes.
- mergeConfig() treats an empty value for a sensitive key as "leave
unchanged" — the admin UI omits unchanged secrets so operators can
tweak non-sensitive settings without re-entering credentials.
- Admin dialog (PaymentProviderDialog.vue):
* secret inputs get autocomplete="new-password", data-1p-ignore,
data-lpignore and data-bwignore so password managers do not
offer to save provider credentials
* in edit mode the required-field check skips sensitive fields
(empty is the "keep existing" signal) and the placeholder shows
"leave empty to keep" instead of the default example value
* create mode still requires every non-optional field, including
secrets, since there is nothing to preserve
- Unit test renamed to TestIsSensitiveProviderConfigField, covers
the per-provider registry and specifically asserts that Stripe's
publishableKey is NOT treated as a secret.
The native Alipay provider previously tried to embed the payment page
URL into a QR code on the client — the URL is not a scannable payload
so the QR never worked. Merchants also hit a H5 detection mismatch
whenever the backend UA sniffer missed iPadOS 13+ or embedded browsers,
and the popup window was too small for Alipay's standard checkout
layout (QR + account-login panel on the right), forcing the user to
scroll horizontally and vertically.
Changes:
Backend
- alipay.go: drop QR-on-URL path. Use redirect-only flow —
alipay.trade.page.pay for PC (returns a gateway URL the browser
opens in a new window) and alipay.trade.wap.pay for H5 (returns a
URL the browser jumps to). Both flows produce pages on
openapi.alipaydev.com / excashier.alipay.com; the client never
renders a QR itself.
- payment_handler.go: add optional is_mobile bool to
CreateOrderRequest so the frontend can declare the device
explicitly. Server still falls back to UA sniffing when absent.
Frontend
- types/payment.ts, PaymentView.vue: declare is_mobile in
CreateOrderRequest and pass the computed isMobileDevice() value.
- providerConfig.ts: replace the two fixed POPUP_WINDOW_FEATURES
constants with getPaymentPopupFeatures(), which prefers 1250×900
(Alipay's checkout footprint), clamps to window.screen.avail* and
centers the popup so it never overflows on smaller laptops.
- PaymentQRDialog.vue, PaymentStatusPanel.vue, StripePaymentInline.vue,
PaymentView.vue: use the new helper at all popup call sites.
Chrome's password manager matched the apikey-type account's Base URL + API Key
inputs as a login form and autofilled the last saved password by domain, so
editing a Gemini account could overwrite its apikey with a Claude key that
shared the same Base URL. Add autocomplete="new-password" plus data-*-ignore
attributes for 1Password / LastPass / Bitwarden to opt the field out of every
major password manager's autofill.
Replace dead EventSource variable with AbortController to enable
cancelling fetch streams. Remove close-button disable during connecting
status so users can dismiss the dialog at any time.
- Show base amount (充值金额) as first line
- Show fee amount with percentage when fee_rate > 0
- Show pay_amount (实付金额) in bold primary color
- Show credited amount (到账金额) when different from pay_amount
- Compute baseAmount and feeAmount from backend order data
Backend:
- Use cfg.RechargeFeeRate in order creation instead of hardcoded 0
- Remove dead getFeeRate stub method
- All amounts computed server-side: order_amount, pay_amount, fee_rate
Frontend - PaymentView:
- Read recharge_fee_rate from checkout-info API (not per-method)
- Show fee breakdown only when fee_rate > 0
- Show credited amount only when multiplier ≠ 1
Frontend - Order display (user + admin):
- Fix fee_rate * 100 bug (fee_rate is already a percentage)
- OrderTable: show pay_amount as primary, fee/credited as sub-lines
- AdminOrderDetail: full breakdown (base/fee/paid/credited)
- AdminRefundDialog: label "到账金额" for clarity
- PaymentResultView: show pay_amount with fee info
Types + i18n:
- Add recharge_fee_rate to CheckoutInfoResponse
- Add fee_rate to CreateOrderResult
- Add translations: creditedAmount, fee, baseAmount, includedInPayAmount