Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 49 additions & 9 deletions dashboard/backend/routes/brain_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,39 @@ def detect():
@bp.route("/api/brain-repo/snapshots")
@login_required
def snapshots():
"""List available restore snapshots (daily / weekly / milestones / head)."""
"""List available restore snapshots (daily / weekly / milestones / head).

Accepts an explicit ``token`` + ``repo_url`` pair (query string) for the
restore-wizard preview flow, which lists snapshots *before* persisting a
BrainRepoConfig. When either is missing it falls back to the stored
config — the path used by ``/settings/brain-repo`` and reconnect flows.

This mirrors the same opt-in token override that ``/api/brain-repo/detect``
already accepts, and unblocks the wizard catch-22 where snapshot preview
used to require a persisted config that the wizard never wrote.
"""
override_token = request.args.get("token", "").strip()
override_repo_url = request.args.get("repo_url", "").strip()

if override_token and override_repo_url:
try:
from brain_repo.github_api import get_repo_info, list_snapshots
except ImportError:
return jsonify({"daily": [], "weekly": [], "milestones": [], "head": None})

_ok, repo_info = get_repo_info(override_token, override_repo_url)
owner = (repo_info.get("owner", {}) or {}).get("login", "")
name = repo_info.get("name", "")
if not owner or not name:
abort(400, description="repo_url did not resolve to a GitHub repo for this token")

try:
result = list_snapshots(override_token, owner, name)
except Exception as exc:
log.warning("snapshots: list_snapshots failed (override): %s", exc)
abort(400, description=f"Could not list snapshots: {exc}")
return jsonify(result)

config = _get_config()
if not config or not config.github_token_encrypted:
abort(400, description="Brain repo not connected")
Expand Down Expand Up @@ -557,20 +589,28 @@ def restore_start():
# master key") — when False, KB import silently degrades to metadata-only,
# which is the safe default.
kb_key_matches = bool(data.get("kb_key_matches", False))
override_token = (data.get("token") or "").strip()
override_repo_url = (data.get("repo_url") or "").strip()

if not ref:
abort(400, description="ref required")

config = _get_config()
if not config or not config.github_token_encrypted:
abort(400, description="Brain repo not connected")
# The wizard restore flow runs before a BrainRepoConfig is persisted, so it
# passes token + repo_url explicitly. /settings/brain-repo and reconnect
# paths still rely on the stored config (no override → falls through).
if override_token and override_repo_url:
token = override_token
repo_url = override_repo_url
else:
config = _get_config()
if not config or not config.github_token_encrypted:
abort(400, description="Brain repo not connected")

token = _decrypt_token(config)
if not token:
abort(400, description="Could not decrypt stored token")
token = _decrypt_token(config)
if not token:
abort(400, description="Could not decrypt stored token")

# Capture needed values before entering generator (avoids app context issues)
repo_url = config.repo_url
repo_url = config.repo_url
# install_dir is where SWAP_DIRS (memory/workspace/customizations/config-safe)
# get replaced — i.e. the EvoNexus workspace root, NOT the brain-repo clone
# path. Confusing these two is what broke the restore endpoint.
Expand Down
16 changes: 13 additions & 3 deletions dashboard/frontend/src/pages/onboarding/restore/RestoreExecute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ interface RestoreStep {

interface RestoreExecuteProps {
snapshot: SelectedSnapshot
token: string
repoUrl: string
onComplete: () => void
onRetry: () => void
}

const API = import.meta.env.DEV ? 'http://localhost:8080' : ''

export default function RestoreExecute({ snapshot, onComplete, onRetry }: RestoreExecuteProps) {
export default function RestoreExecute({ snapshot, token, repoUrl, onComplete, onRetry }: RestoreExecuteProps) {
const { t } = useTranslation()
const [progress, setProgress] = useState(0)
const [steps, setSteps] = useState<RestoreStep[]>([])
Expand All @@ -38,11 +40,19 @@ export default function RestoreExecute({ snapshot, onComplete, onRetry }: Restor

const run = async () => {
try {
// Forward token + repoUrl alongside the snapshot ref so the backend
// can resolve the GitHub remote without a persisted BrainRepoConfig
// (the wizard runs before /connect has been called — catch-22 fix).
const res = await fetch(`${API}/api/brain-repo/restore/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
body: JSON.stringify({ ref: snapshot.ref, include_kb: snapshot.includeKb }),
body: JSON.stringify({
ref: snapshot.ref,
include_kb: snapshot.includeKb,
token,
repo_url: repoUrl,
}),
signal: ctrl.signal,
})

Expand Down Expand Up @@ -135,7 +145,7 @@ export default function RestoreExecute({ snapshot, onComplete, onRetry }: Restor

run()
return () => ctrl.abort()
}, [snapshot, onComplete, t])
}, [snapshot, token, repoUrl, onComplete, t])

return (
<div className="min-h-screen bg-[#080c14] flex items-center justify-center px-4 font-[Inter,-apple-system,sans-serif]">
Expand Down
11 changes: 10 additions & 1 deletion dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ interface RestoreFlowProps {
export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) {
const [step, setStep] = useState<RestoreStep>('select-repo')
const [repoUrl, setRepoUrl] = useState('')
// Token + repoUrl flow through every step because /api/brain-repo/snapshots
// and /api/brain-repo/restore/start need them: in the wizard there is no
// persisted BrainRepoConfig yet, so the endpoints accept the pair explicitly
// (catch-22 fix — see routes/brain_repo.py).
const [token, setToken] = useState('')
const [snapshot, setSnapshot] = useState<SelectedSnapshot | null>(null)

if (step === 'select-repo') {
return (
<RestoreSelectRepo
onNext={(url: string) => {
onNext={(url: string, t: string) => {
setRepoUrl(url)
setToken(t)
setStep('select-snapshot')
}}
onBack={onBack}
Expand All @@ -38,6 +44,7 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) {
return (
<RestoreSelectSnapshot
repoUrl={repoUrl}
token={token}
onNext={(s: SelectedSnapshot) => {
setSnapshot(s)
setStep('confirm')
Expand All @@ -61,6 +68,8 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) {
return (
<RestoreExecute
snapshot={snapshot}
token={token}
repoUrl={repoUrl}
onComplete={onComplete}
onRetry={() => setStep('confirm')}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Repo {
}

interface RestoreSelectRepoProps {
onNext: (repoUrl: string) => void
onNext: (repoUrl: string, token: string) => void
onBack: () => void
}

Expand Down Expand Up @@ -65,7 +65,7 @@ export default function RestoreSelectRepo({ onNext, onBack }: RestoreSelectRepoP
setError(t('restore.selectRepo.selectRepo'))
return
}
onNext(selectedRepo.html_url)
onNext(selectedRepo.html_url, token.trim())
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface SelectedSnapshot {

interface RestoreSelectSnapshotProps {
repoUrl: string
token: string
onNext: (snapshot: SelectedSnapshot) => void
onBack: () => void
}
Expand Down Expand Up @@ -62,7 +63,7 @@ function SnapshotItem({
)
}

export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: RestoreSelectSnapshotProps) {
export default function RestoreSelectSnapshot({ repoUrl, token, onNext, onBack }: RestoreSelectSnapshotProps) {
const { t } = useTranslation()
const [data, setData] = useState<SnapshotData | null>(null)
const [loading, setLoading] = useState(true)
Expand All @@ -71,11 +72,19 @@ export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: Resto
const [error, setError] = useState('')

useEffect(() => {
api.get('/brain-repo/snapshots')
// Pass token + repo_url explicitly because the wizard runs before a
// BrainRepoConfig is persisted (restore is a first-run path). The
// endpoint still falls back to stored config when these are absent.
const params = new URLSearchParams()
if (token) params.set('token', token)
if (repoUrl) params.set('repo_url', repoUrl)
const qs = params.toString()
const url = qs ? `/brain-repo/snapshots?${qs}` : '/brain-repo/snapshots'
api.get(url)
.then((d: SnapshotData) => setData(d))
.catch(() => setError(t('restore.selectSnapshot.failed')))
.finally(() => setLoading(false))
}, [repoUrl, t])
}, [repoUrl, token, t])

const handleNext = () => {
if (!selected) {
Expand Down