fix(mcp): explain the Claude-side steps after "Anslut till Claude" and tick the checklist on a real connection (#2133) (#2147)
* fix(mcp): explain the Claude-side steps after "Anslut till Claude" and tick the checklist on a real connection (#2133) Lazy auth is by design: Claude lists the tools before any sign-in and the first company-scoped call answers 401, which opens the Accounted sign-in. Nothing told the user, so a "connected" status with an unanswered first question read as a broken connection (Axel, Discord). - Settings -> API & MCP: one sentence of expectation under the button, and the step-by-step guide link moved from under two disclosures to directly under the button. - Docs (connect-claude / anslut-claude): new "What happens after you click" section for Path A covering the connector dialog, the tools appearing before sign-in, the first-call login + consent screen, "ask again", and the "Required when the server asks" auth setting that only the manual path mentioned. - Hem checklist step "Anslut till Claude": deep link now carries client=claude-connector like the settings button (claudeConnectorLink), the footnote carries the same expectation line plus the guide link, and the done-signal is an unrevoked api_keys row minted by the MCP OAuth token route (OAUTH_MCP_KEY_NAME) instead of the in-app AI-profile flag, which never meant "connected to Claude". - Tests: claudeStepDone with/without a key row, deep-link snapshot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7iJQwKiRTDWSMnRm4WM4L * fix(mcp): correct consent-page claims, stop the completion PATCH loop, count OAuth keys past RLS (#2133) Three skeptic refutations on PR #2147, fixed in one pass: - Docs (EN + SV): the consent page shows the company active in the app and pre-selects every scope for Claude's connector (founder decision 2026-08-26); it has no company picker and nothing to tick. Steps 3-4 of the new section, the "Read-only by default" paragraph above it, the sandbox note and the 10-minute test now describe Endast läs under Behörigheter instead. - Checklist completion: users with initial_setup_path NULL (skipped the books question, then imported) hit the route's "Välj först hur du vill komma igång" 400 and, with saving as an effect dependency, retried it forever with a toast. completionPatchBody() records path=migration when none was chosen, and a rejected PATCH is not retried within the session. - hasMcpKey: api_keys' SELECT policy is company-scoped, so the user client could not see companyless (NULL company_id) or archived-company keys and the step stayed open for the user who had just connected. The head count now runs through the service client with an explicit user_id filter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7iJQwKiRTDWSMnRm4WM4L * fix(mcp): surface a failed OAuth-key count and reserve the marker name (#2133) CodeRabbit round on PR #2147: - app/(dashboard)/page.tsx: a failed api_keys count answered count null, which claudeStepDone read as "never connected". Throw to the error boundary like the settings fetch does instead of guessing. - app/api/settings/api-keys: reject a hand-minted key named MCP-klient (OAuth) (400 VALIDATION_ERROR): that name is the marker the Hem checklist reads as "connected to Claude", so a manual key with it would tick the step without any connection. Test added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7iJQwKiRTDWSMnRm4WM4L --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8b09b06e14
commit
4f33184a9a
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { Check } from 'lucide-react'
|
||||
import posthog from 'posthog-js'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -13,7 +13,12 @@ import { cn } from '@/lib/utils'
|
||||
import { useErrorToast } from '@/lib/hooks/use-error-toast'
|
||||
import { useFormat } from '@/lib/hooks/use-format'
|
||||
import { isAnalyticsEnabled } from '@/lib/analytics/enabled'
|
||||
import { checklistNumbers, type VatDeadlineLine } from '@/lib/onboarding/checklist'
|
||||
import {
|
||||
checklistNumbers,
|
||||
claudeConnectorLink,
|
||||
completionPatchBody,
|
||||
type VatDeadlineLine,
|
||||
} from '@/lib/onboarding/checklist'
|
||||
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
import { useCapability } from '@/contexts/CompanyContext'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
@@ -27,7 +32,11 @@ interface NewUserChecklistProps {
|
||||
hasBankConnected?: boolean
|
||||
hasSkatteverketConnected?: boolean
|
||||
hasInboxItems?: boolean
|
||||
hasAgentBuilt?: boolean
|
||||
/** The user holds a live OAuth-minted MCP key, i.e. a Claude (or other
|
||||
* MCP client) connection completed its first sign-in. This is the only
|
||||
* signal that means "connected to Claude"; the in-app AI-profile flag
|
||||
* used to tick this step and never corresponded to it (issue #2133). */
|
||||
hasMcpKey?: boolean
|
||||
/** Personalized VAT-deadline line for the Skatteverket step (null = say nothing). */
|
||||
vatLine?: VatDeadlineLine
|
||||
/** Latest SIE reconciliation-sweep outcome: surfaces "X matchade, Y att
|
||||
@@ -74,11 +83,12 @@ export default function NewUserChecklist({
|
||||
hasBankConnected = false,
|
||||
hasSkatteverketConnected = false,
|
||||
hasInboxItems = false,
|
||||
hasAgentBuilt = false,
|
||||
hasMcpKey = false,
|
||||
vatLine = null,
|
||||
sieSweep = null,
|
||||
}: NewUserChecklistProps) {
|
||||
const t = useTranslations('initial_setup')
|
||||
const locale = useLocale()
|
||||
const { appName } = useBranding()
|
||||
const router = useRouter()
|
||||
const showError = useErrorToast()
|
||||
@@ -92,6 +102,12 @@ export default function NewUserChecklist({
|
||||
// step (companies whose completedAt arrives from the server never see it).
|
||||
const [retiring, setRetiring] = useState<'verdict' | 'closing' | 'done' | null>(null)
|
||||
const retireStartedRef = useRef(false)
|
||||
// A completion PATCH the server rejected (4xx: no write role, no settings
|
||||
// row) must not be retried in a loop: `saving` is a dependency of the
|
||||
// completion effect, so without this latch every rejection re-armed the
|
||||
// effect and re-raised the error toast forever. The next visit tries once
|
||||
// more from server truth.
|
||||
const completeRejectedRef = useRef(false)
|
||||
// The ChatGPT side door on the Claude step: collapsed by default so the
|
||||
// one-click Claude path stays the visual primary.
|
||||
const [chatGptOpen, setChatGptOpen] = useState(false)
|
||||
@@ -134,7 +150,7 @@ export default function NewUserChecklist({
|
||||
// Companies built without the skatteverket/inbox extensions skip those steps.
|
||||
const step3Done = !hasSkatteverket || hasSkatteverketConnected
|
||||
const step4Done = !hasInbox || hasInboxItems
|
||||
const step5Done = hasAgentBuilt
|
||||
const step5Done = hasMcpKey
|
||||
|
||||
useEffect(() => {
|
||||
// The block retires itself once every step is done; Dölj remains the
|
||||
@@ -143,14 +159,18 @@ export default function NewUserChecklist({
|
||||
if (
|
||||
!state.completedAt &&
|
||||
step1Done && step2Done && step3Done && step4Done && step5Done &&
|
||||
saving === null
|
||||
saving === null &&
|
||||
!completeRejectedRef.current
|
||||
) {
|
||||
if (!retireStartedRef.current) {
|
||||
retireStartedRef.current = true
|
||||
setRetiring('verdict')
|
||||
}
|
||||
void persist({ completed: true }, 'complete').then((updated) => {
|
||||
// completionPatchBody supplies a path when none was recorded: the route
|
||||
// refuses completed:true without one, and this cohort looped on a 400.
|
||||
void persist(completionPatchBody(state.path), 'complete').then((updated) => {
|
||||
if (updated) captureSetup('onboarding_setup_completed', { path: updated.path })
|
||||
else completeRejectedRef.current = true
|
||||
})
|
||||
}
|
||||
// persist intentionally stays out: its identity follows the toast hook and
|
||||
@@ -175,8 +195,9 @@ export default function NewUserChecklist({
|
||||
if (state.dismissedAt) return null
|
||||
// After the beat, stay retired even while the completion PATCH is still in
|
||||
// flight or retrying: falling through to the full checklist here would
|
||||
// flash it after the verdict already played. A failed PATCH keeps retrying
|
||||
// invisibly; the next visit renders from server truth either way.
|
||||
// flash it after the verdict already played. A rejected PATCH is not
|
||||
// retried this session (completeRejectedRef); the next visit renders from
|
||||
// server truth either way.
|
||||
if (retiring === 'done') return null
|
||||
if (state.completedAt && !retiring) return null
|
||||
|
||||
@@ -231,9 +252,11 @@ export default function NewUserChecklist({
|
||||
// page origin so self-hosted and white-label domains link to themselves.
|
||||
const goClaude = () => {
|
||||
captureSetup('onboarding_setup_step_started', { step: 'claude' })
|
||||
const serverUrl = `${window.location.origin}/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted`
|
||||
const link = `https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=${encodeURIComponent(appName)}&connectorUrl=${encodeURIComponent(serverUrl)}`
|
||||
window.open(link, '_blank', 'noopener')
|
||||
window.open(
|
||||
claudeConnectorLink({ origin: window.location.origin, appName }),
|
||||
'_blank',
|
||||
'noopener',
|
||||
)
|
||||
}
|
||||
// ChatGPT has no add-connector deep link (the user pastes the server URL
|
||||
// into Developer mode manually), so the side door copies the URL instead.
|
||||
@@ -428,6 +451,22 @@ export default function NewUserChecklist({
|
||||
)}
|
||||
footnote={
|
||||
<div className="mt-2">
|
||||
{/* What the click leads to on Claude's side. Tools list before
|
||||
any sign-in (lazy auth, by design), so without this line a
|
||||
"connected" status with an unanswered first question reads
|
||||
as a broken connection (issue #2133). The guide carries the
|
||||
full sequence; one language per URL, see ApiKeysPanel. */}
|
||||
<p className="mb-2 max-w-prose text-xs leading-5 text-muted-foreground">
|
||||
{t('step_claude_expectation')}{' '}
|
||||
<a
|
||||
href={locale === 'sv' ? '/docs/api/anslut-claude' : '/docs/api/connect-claude'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
|
||||
>
|
||||
{t('step_claude_guide_link')}
|
||||
</a>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChatGpt}
|
||||
|
||||
@@ -375,6 +375,22 @@ export function ApiKeysPanel() {
|
||||
<p className="mt-2 max-w-prose text-xs text-muted-foreground">
|
||||
{t('connect_to_claude_help')}
|
||||
</p>
|
||||
{/* The step-by-step guide is canonical on the docs site, in one
|
||||
language per URL (the docs site has no locale routing). Root-relative
|
||||
so the /docs/api/* 308 in next.config.ts forwards to docs.gnubok.se.
|
||||
It sits right under the button: the steps on Claude's side after
|
||||
the click (consent, first-call sign-in) live there, and a reader
|
||||
who has just clicked should not have to open two disclosures to
|
||||
find them (issue #2133). */}
|
||||
<a
|
||||
href={locale === 'sv' ? '/docs/api/anslut-claude' : '/docs/api/connect-claude'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 inline-flex items-center gap-1.5 text-xs text-muted-foreground underline-offset-4 transition-colors duration-150 hover:text-foreground hover:underline"
|
||||
>
|
||||
{t('full_guide_link')}
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -493,19 +509,6 @@ export function ApiKeysPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</SettingsReveal>
|
||||
|
||||
{/* The step-by-step guide is canonical on the docs site, in one
|
||||
language per URL (the docs site has no locale routing). Root-relative
|
||||
so the /docs/api/* 308 in next.config.ts forwards to docs.gnubok.se. */}
|
||||
<a
|
||||
href={locale === 'sv' ? '/docs/api/anslut-claude' : '/docs/api/connect-claude'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-1 py-3 text-xs text-muted-foreground underline-offset-4 transition-colors duration-150 hover:text-foreground hover:underline"
|
||||
>
|
||||
{t('full_guide_link')}
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
</a>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup>
|
||||
|
||||
Reference in New Issue
Block a user