Files
sencho/frontend/src/App.tsx
T
Anso 27b8954676 feat(deploy-panel): tell Community operators deploys lack auto-rollback (#1193)
* feat(deploy-panel): tell Community operators deploys lack auto-rollback

Atomic-deploy is paid-only (effectiveTier === 'paid' in the deploy and
update routes); Community deploys proceed without the backup/restore
fallback. The UI never told the user. They only learned the difference
when a deploy failed and there was nothing to roll back to.

Add a one-line muted-style notice strip inside the deploy-feedback
modal, between header and log body, shown only when the user is on
Community AND the action is a deploy or update (the two paths that
support atomic on paid).

Copy is deliberately one line and states the requirement once:

  "Auto-rollback on failure is a Skipper feature."

Compliant with Directive 31: it does not enumerate where the feature
is hidden, it does not say "you don't get it", it states what the
upgrade unlocks. Other tier-named upgrade prompts in Sencho follow
the same pattern.

Resolves M-3 from the stack-management audit.

* fix(deploy-panel): mount DeployFeedbackPortal inside LicenseProvider

The portal was mounted at App level, outside the authed AppContent
tree where LicenseProvider lives. After this PR introduced
useLicense() inside DeployFeedbackModal (for the atomic-deploy
notice), every test that opened the modal hit:

  Error: useLicense must be used within a LicenseProvider

caught by ErrorBoundary and surfaced through every deploy-log-panel
E2E spec.

Move the portal inside LicenseProvider in AppContent. DeployFeedback-
Provider stays at App level so its state survives across re-renders
of AppContent; the portal still inherits it because AppContent is a
descendant.

A deploy can only fire after authentication, so rendering the portal
only inside the authed tree loses nothing in practice.
2026-05-24 15:56:40 -04:00

60 lines
1.7 KiB
TypeScript

import { AuthProvider, useAuth } from './context/AuthContext';
import { NodeProvider } from './context/NodeContext';
import { LicenseProvider } from './context/LicenseContext';
import { Login } from './components/Login';
import { Setup } from './components/Setup';
import EditorLayout from './components/EditorLayout';
import { MfaChallenge } from './components/MfaChallenge';
import { DeployFeedbackProvider } from './context/DeployFeedbackContext';
import { DeployFeedbackPortal } from './components/DeployFeedbackPortal';
import { ToastContainer } from './components/ui/toast';
function AppContent() {
const { appStatus, isAuthenticated, needsSetup, completeSetup } = useAuth();
if (appStatus === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-muted-foreground">Loading...</div>
</div>
);
}
if (needsSetup) {
return <Setup onComplete={completeSetup} />;
}
if (appStatus === 'mfaChallenge') {
return <MfaChallenge />;
}
if (!isAuthenticated) {
return <Login />;
}
return (
<NodeProvider>
<LicenseProvider>
<EditorLayout />
{/* Portal lives inside LicenseProvider so DeployFeedbackModal can
call useLicense() (M-3 atomic-deploy notice depends on isPaid).
Outer DeployFeedbackProvider is still an ancestor through App. */}
<DeployFeedbackPortal />
</LicenseProvider>
</NodeProvider>
);
}
function App() {
return (
<AuthProvider>
<DeployFeedbackProvider>
<AppContent />
</DeployFeedbackProvider>
<ToastContainer />
</AuthProvider>
);
}
export default App;