remove projects and apps end to end

deletes the last of the projects/apps cluster: the published-app store
(/api/apps + /api/app-serve), the project dev-server and its websocket
proxy (/api/dev-server + /api/dev-server-proxy), the shared html-rewrite
they were the only consumers of, and their frontend — the Preview panel,
the UserApp panel/header, useUserApps and the /settings/apps screen.

also drops getUserProjectsDir and getUserAppsDir, the ProjectType and
ProjectDefinition types, and the 'dev-server' websocket provider from
server.tsx. nothing on disk is touched.

1440 deletions, 31 insertions. tsgo clean.
This commit is contained in:
2026-07-31 07:39:29 +00:00
parent 13b7f56b0f
commit 188b45c113
28 changed files with 31 additions and 1440 deletions
-1
View File
@@ -34,7 +34,6 @@ export function App() {
<Route path="/settings/ai" element={<Dashboard.AISettings />} />
<Route path="/settings/system" element={<Dashboard.SystemSettings />} />
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
<Route path="/chat" element={<Dashboard.SessionListPage />} />
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
<Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
@@ -1,7 +1,7 @@
import { Link } from 'react-router';
import * as Dropdown from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { User, LogOut, Settings, Package, Puzzle, Rocket, Sun, Moon, Bot } from 'lucide-react';
import { User, LogOut, Settings, Package, Puzzle, Sun, Moon, Bot } from 'lucide-react';
import { useAuth } from 'hooks/useAuth';
import { useTranslation } from '@/lib/i18n';
import { useColorMode } from '@/components/ui/ThemeProvider';
@@ -65,12 +65,6 @@ export function UserMenu() {
Integrations
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/apps">
<Rocket className="mr-2 h-4 w-4" />
Apps
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={toggleColorMode} className="cursor-pointer">
{colorMode === 'dark' ? <Sun className="mr-2 h-4 w-4" /> : <Moon className="mr-2 h-4 w-4" />}
@@ -1,115 +0,0 @@
import { useState } from 'react';
import { Trash2, Loader2, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';
import { useUserApps, type AppManifest } from 'state/useUserApps';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
export const AppsList = () => {
const client = useClient();
const { apps, isLoading, refetch } = useUserApps();
const [deleting, setDeleting] = useState<AppManifest | null>(null);
const [deletingInProgress, setDeletingInProgress] = useState(false);
const confirmDelete = async () => {
if (!deleting) return;
setDeletingInProgress(true);
try {
await client.delete(`/apps/${deleting.slug}`);
toast.success(`Deleted ${deleting.name}`);
refetch();
} catch {
toast.error('Failed to delete app');
} finally {
setDeletingInProgress(false);
setDeleting(null);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-duck-dark/30 dark:text-foreground/30" />
</div>
);
}
if (apps.length === 0) {
return (
<div className="text-sm text-duck-dark/40 dark:text-foreground/40 py-8 text-center">
No published apps yet. Publish a project from the Projects page.
</div>
);
}
return (
<>
<div className="flex flex-col gap-2">
{apps.map((app) => (
<div
key={app.slug}
className="flex items-center gap-4 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{app.name}</span>
<span className="text-[10px] font-mono text-duck-dark/30 dark:text-foreground/30">v{app.version}</span>
</div>
{app.description && (
<p className="text-xs text-duck-dark/50 dark:text-foreground/50 mt-0.5 truncate">{app.description}</p>
)}
<div className="flex items-center gap-3 mt-1.5 text-[11px] text-duck-dark/30 dark:text-foreground/30">
<span>Source: <span className="font-mono">{app.sourceProject}</span></span>
<span>Commit: <span className="font-mono">{app.commitHash}</span></span>
<span>Published: {new Date(app.publishedAt).toLocaleDateString()}</span>
</div>
</div>
<a
href={`/api/app-serve/${app.slug}/`}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 p-2 rounded-md text-duck-dark/30 dark:text-foreground/30 hover:text-duck-teal hover:bg-duck-teal/10 transition-colors cursor-pointer"
title="Open in new tab"
>
<ExternalLink className="h-4 w-4" />
</a>
<button
type="button"
onClick={() => setDeleting(app)}
className="shrink-0 p-2 rounded-md text-duck-dark/30 dark:text-foreground/30 hover:text-red-500 hover:bg-red-500/10 transition-colors cursor-pointer"
title="Delete app"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
</div>
<AlertDialog open={deleting !== null} onOpenChange={(open) => { if (!open) setDeleting(null); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete published app</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete <strong>{deleting?.name}</strong>? This removes the published build. The source project is not affected.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deletingInProgress}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} disabled={deletingInProgress} className="bg-red-600 hover:bg-red-700">
{deletingInProgress ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
@@ -1,46 +0,0 @@
import { useMemo } from 'react';
import { Rocket, List } from 'lucide-react';
import type { LayoutNode, PanelComponents } from 'officerdev';
import { WorkspaceLayout } from 'officerdev';
import { createSettingsPanelComponents, type SettingsSection } from '../SettingsPanel';
import { AppsList } from './AppsList';
const GLOBAL_KEY = 'APPS_SETTINGS_SELECTED';
const sections: SettingsSection[] = [
{ key: 'published-apps', icon: List, title: 'Published Apps', description: 'View and manage your published apps', content: <AppsList /> },
];
const { Sidebar, Content } = createSettingsPanelComponents({
globalKey: GLOBAL_KEY,
sidebarIcon: Rocket,
sidebarLabel: 'Apps',
sections,
});
const layout: LayoutNode = {
type: 'group',
id: 'apps-settings-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'apps-settings-left', appType: null }, size: 20 },
{ node: { type: 'panel', id: 'apps-settings-right', appType: null }, size: 80 },
],
};
export const AppsSettings = () => {
const panelComponents: PanelComponents = useMemo(
() => ({
'apps-settings-left': Sidebar,
'apps-settings-right': Content,
}),
[],
);
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
);
};
@@ -2,4 +2,3 @@ export * from './ProfileSettings';
export * from './SystemSettings';
export * from './AISettings';
export * from './IntegrationsSettings';
export * from './AppsSettings';
@@ -10,7 +10,6 @@ const RULES: TitleRule[] = [
{ match: (p) => p.startsWith('/settings/ai'), title: 'AI Settings' },
{ match: (p) => p.startsWith('/settings/profile'), title: 'Profile' },
{ match: (p) => p.startsWith('/settings/integrations'), title: 'Integrations' },
{ match: (p) => p.startsWith('/settings/apps'), title: 'App Settings' },
{ match: (p) => p.startsWith('/settings'), title: 'Settings' },
{ match: (p) => p.startsWith('/chat'), title: 'Chat' },
{ match: (p) => p.startsWith('/email'), title: 'Email' },