feat: RBAC, atomic deployments, and fleet-wide backups (Pro) (#181)

* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)

Introduces three Pro-tier features:

- RBAC: Multi-user system with admin/viewer roles, user management UI,
  automatic migration from single-admin credentials, viewer restrictions
  across the entire UI (read-only editor, hidden action buttons)

- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
  rollback on health probe failure, manual rollback button, health probes
  added to stack updates, webhook-triggered deploys use atomic rollback

- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
  nodes (local + remote), stored centrally in SQLite, per-stack restore
  with optional redeploy, graceful handling of offline nodes

* fix(settings): use correct ProGate prop name in UsersSection

* fix(settings): remove unused isPro prop from UsersSection

* fix(auth): fetch user info after login and setup so isAdmin is set correctly
This commit is contained in:
Anso
2026-03-26 12:51:30 -04:00
committed by GitHub
parent 72670ffb42
commit db73d7671a
21 changed files with 2466 additions and 292 deletions
+4 -1
View File
@@ -10,6 +10,7 @@ import { Search, Rocket, Loader2, Info, ExternalLink, Star } from "lucide-react"
import { toast } from "sonner";
import { apiFetch } from '@/lib/api';
import { useNodes } from '@/context/NodeContext';
import { useAuth } from '@/context/AuthContext';
export interface TemplateEnv {
name: string;
@@ -44,6 +45,7 @@ interface AppStoreViewProps {
}
export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
const { isAdmin } = useAuth();
const { activeNode } = useNodes();
const [templates, setTemplates] = useState<Template[]>([]);
const [searchQuery, setSearchQuery] = useState('');
@@ -471,9 +473,10 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
<div className="flex flex-col w-full gap-2">
<Button
onClick={handleDeploy}
disabled={isDeploying || !stackName.trim()}
disabled={isDeploying || !stackName.trim() || !isAdmin}
className="w-full"
size="lg"
title={!isAdmin ? 'Admin access required to deploy' : undefined}
>
{isDeploying ? (
<>