Skip to content
Merged
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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,18 @@ To upgrade Archon to the latest version:
git pull
```

2. **Check for migrations**: Look in the `migration/` folder for any SQL files newer than your last update. Check the file created dates to determine if you need to run them. You can run these in the SQL editor just like you did when you first set up Archon. We are also working on a way to make handling these migrations automatic!

3. **Rebuild and restart**:
2. **Rebuild and restart containers**:
```bash
docker compose up -d --build
```

This is the same command used for initial setup - it rebuilds containers with the latest code and restarts services.
This rebuilds containers with the latest code and restarts all services.

3. **Check for database migrations**:
- Open the Archon settings in your browser: [http://localhost:3737/settings](http://localhost:3737/settings)
- Navigate to the **Database Migrations** section
- If there are pending migrations, the UI will display them with clear instructions
- Click on each migration to view and copy the SQL
- Run the SQL scripts in your Supabase SQL editor in the order shown

## What's Included

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Card component showing migration status
*/

import { motion } from "framer-motion";
import { AlertTriangle, CheckCircle, Database, RefreshCw } from "lucide-react";
import React from "react";
import { useMigrationStatus } from "../hooks/useMigrationQueries";
import { PendingMigrationsModal } from "./PendingMigrationsModal";

export function MigrationStatusCard() {
const { data, isLoading, error, refetch } = useMigrationStatus();
const [isModalOpen, setIsModalOpen] = React.useState(false);

const handleRefresh = () => {
refetch();
};

return (
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Database className="w-5 h-5 text-purple-400" />
<h3 className="text-white font-semibold">Database Migrations</h3>
</div>
<button type="button"
onClick={handleRefresh}
disabled={isLoading}
className="p-2 hover:bg-gray-700/50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Refresh migration status"
>
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? "animate-spin" : ""}`} />
</button>
</div>

<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Applied Migrations</span>
<span className="text-white font-mono text-sm">{data?.applied_count ?? 0}</span>
</div>

<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Pending Migrations</span>
<div className="flex items-center gap-2">
<span className="text-white font-mono text-sm">{data?.pending_count ?? 0}</span>
{data && data.pending_count > 0 && <AlertTriangle className="w-4 h-4 text-yellow-400" />}
</div>
</div>

<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Status</span>
<div className="flex items-center gap-2">
{isLoading ? (
<>
<RefreshCw className="w-4 h-4 text-blue-400 animate-spin" />
<span className="text-blue-400 text-sm">Checking...</span>
</>
) : error ? (
<>
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-red-400 text-sm">Error loading</span>
</>
) : data?.bootstrap_required ? (
<>
<AlertTriangle className="w-4 h-4 text-yellow-400" />
<span className="text-yellow-400 text-sm">Setup required</span>
</>
) : data?.has_pending ? (
<>
<AlertTriangle className="w-4 h-4 text-yellow-400" />
<span className="text-yellow-400 text-sm">Migrations pending</span>
</>
) : (
<>
<CheckCircle className="w-4 h-4 text-green-400" />
<span className="text-green-400 text-sm">Up to date</span>
</>
)}
</div>
</div>

{data?.current_version && (
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Database Version</span>
<span className="text-white font-mono text-sm">{data.current_version}</span>
</div>
)}
</div>

{data?.has_pending && (
<div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<p className="text-yellow-400 text-sm mb-2">
{data.bootstrap_required
? "Initial database setup is required."
: `${data.pending_count} migration${data.pending_count > 1 ? "s" : ""} need to be applied.`}
</p>
<button type="button"
onClick={() => setIsModalOpen(true)}
className="px-3 py-1.5 bg-yellow-500/20 hover:bg-yellow-500/30 border border-yellow-500/50 rounded text-yellow-400 text-sm font-medium transition-colors"
>
View Pending Migrations
</button>
</div>
)}
Comment on lines +96 to +110
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid sending full SQL in the status query; fetch only when needed

Status payload includes pending_migrations with full SQL, which can be large. Prefer lightweight status (counts/flags) and lazy‑load /pending when opening the modal.

-        {data && (
-        <PendingMigrationsModal
-          isOpen={isModalOpen}
-          onClose={() => setIsModalOpen(false)}
-          migrations={data.pending_migrations}
-          onMigrationsApplied={refetch}
-        />
-      )}
+        {/* Load pending migrations only when modal is open */}
+        {isModalOpen && <LazyPendingMigrations refetchStatus={refetch} />}

Additional component (in this feature) to add:

// LazyPendingMigrations.tsx
import React from "react";
import { usePendingMigrations } from "../hooks/useMigrationQueries";
import { PendingMigrationsModal } from "./PendingMigrationsModal";

export function LazyPendingMigrations({ refetchStatus }: { refetchStatus: () => void }) {
  const { data } = usePendingMigrations({ enabled: true });
  const [open, setOpen] = React.useState(true);
  return (
    <PendingMigrationsModal
      isOpen={open}
      onClose={() => setOpen(false)}
      migrations={data ?? []}
      onMigrationsApplied={refetchStatus}
    />
  );
}

Update the backend /status to omit pending_migrations (keep counts) if it currently returns the SQL list.

🤖 Prompt for AI Agents
In
archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx
around lines 96 to 110, the component currently renders a status payload that
may include full SQL for pending_migrations; change it to only show counts/flags
and lazy-load the full SQL when the user opens the modal: remove any usage of
data.pending_migrations here, keep data.has_pending, data.bootstrap_required and
data.pending_count for display, and onClick set a local state (e.g.,
setIsPendingModalOpen(true)) to mount a new LazyPendingMigrations component; add
the suggested LazyPendingMigrations component that calls usePendingMigrations({
enabled: true }) and controls the PendingMigrationsModal open state, and ensure
the modal's onClose unmounts it and onMigrationsApplied calls refetchStatus;
finally update the backend /status endpoint to stop returning the
pending_migrations SQL list (keep counts/flags) so the frontend status request
remains lightweight.


{error && (
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<p className="text-red-400 text-sm">
Failed to load migration status. Please check your database connection.
</p>
</div>
)}
</motion.div>

{/* Modal for viewing pending migrations */}
{data && (
<PendingMigrationsModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
migrations={data.pending_migrations}
onMigrationsApplied={refetch}
/>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* Modal for viewing and copying pending migration SQL
*/

import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle, Copy, Database, ExternalLink, X } from "lucide-react";
import React from "react";
import { copyToClipboard } from "@/features/shared/utils/clipboard";
import { useToast } from "@/features/ui/hooks/useToast";
import type { PendingMigration } from "../types";

interface PendingMigrationsModalProps {
isOpen: boolean;
onClose: () => void;
migrations: PendingMigration[];
onMigrationsApplied: () => void;
}

export function PendingMigrationsModal({
isOpen,
onClose,
migrations,
onMigrationsApplied,
}: PendingMigrationsModalProps) {
const { showToast } = useToast();
const [copiedIndex, setCopiedIndex] = React.useState<number | null>(null);
const [expandedIndex, setExpandedIndex] = React.useState<number | null>(null);

const handleCopy = async (sql: string, index: number) => {
const result = await copyToClipboard(sql);
if (result.success) {
setCopiedIndex(index);
showToast("SQL copied to clipboard", "success");
setTimeout(() => setCopiedIndex(null), 2000);
} else {
showToast("Failed to copy SQL", "error");
}
};

const handleCopyAll = async () => {
const allSql = migrations.map((m) => `-- ${m.name}\n${m.sql_content}`).join("\n\n");
const result = await copyToClipboard(allSql);
if (result.success) {
showToast("All migration SQL copied to clipboard", "success");
} else {
showToast("Failed to copy SQL", "error");
}
};

if (!isOpen) return null;

return (
<AnimatePresence>
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>

{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="relative bg-gray-900 border border-gray-700 rounded-lg shadow-xl w-full max-w-4xl max-h-[80vh] overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-700">
<div className="flex items-center gap-3">
<Database className="w-6 h-6 text-purple-400" />
<h2 className="text-xl font-semibold text-white">Pending Database Migrations</h2>
</div>
<button type="button" onClick={onClose} className="p-2 hover:bg-gray-700/50 rounded-lg transition-colors">
<X className="w-5 h-5 text-gray-400" />
</button>
</div>

{/* Instructions */}
<div className="p-6 bg-blue-500/10 border-b border-gray-700">
<h3 className="text-blue-400 font-medium mb-2 flex items-center gap-2">
<ExternalLink className="w-4 h-4" />
How to Apply Migrations
</h3>
<ol className="text-sm text-gray-300 space-y-1 list-decimal list-inside">
<li>Copy the SQL for each migration below</li>
<li>Open your Supabase dashboard SQL Editor</li>
<li>Paste and execute each migration in order</li>
<li>Click "Refresh Status" below to verify migrations were applied</li>
</ol>
{migrations.length > 1 && (
<button type="button"
onClick={handleCopyAll}
className="mt-3 px-3 py-1.5 bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/50 rounded text-blue-400 text-sm font-medium transition-colors"
>
Copy All Migrations
</button>
)}
</div>

{/* Migration List */}
<div className="overflow-y-auto max-h-[calc(80vh-280px)] p-6 pb-8">
{migrations.length === 0 ? (
<div className="text-center py-8">
<CheckCircle className="w-12 h-12 text-green-400 mx-auto mb-3" />
<p className="text-gray-300">All migrations have been applied!</p>
</div>
) : (
<div className="space-y-4 pb-4">
{migrations.map((migration, index) => (
<div
key={`${migration.version}-${migration.name}`}
className="bg-gray-800/50 border border-gray-700 rounded-lg"
>
<div className="p-4">
<div className="flex items-center justify-between mb-2">
<div>
<h4 className="text-white font-medium">{migration.name}</h4>
<p className="text-gray-400 text-sm mt-1">
Version: {migration.version} • {migration.file_path}
</p>
</div>
<div className="flex items-center gap-2">
<button type="button"
onClick={() => handleCopy(migration.sql_content, index)}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium text-gray-300 flex items-center gap-2 transition-colors"
>
{copiedIndex === index ? (
<>
<CheckCircle className="w-4 h-4 text-green-400" />
Copied!
</>
) : (
<>
<Copy className="w-4 h-4" />
Copy SQL
</>
)}
</button>
<button type="button"
onClick={() => setExpandedIndex(expandedIndex === index ? null : index)}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium text-gray-300 transition-colors"
>
{expandedIndex === index ? "Hide" : "Show"} SQL
</button>
</div>
</div>

{/* SQL Content */}
<AnimatePresence>
{expandedIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<pre className="mt-3 p-3 bg-gray-900 border border-gray-700 rounded text-xs text-gray-300 overflow-x-auto">
<code>{migration.sql_content}</code>
</pre>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
))}
</div>
)}
</div>

{/* Footer */}
<div className="p-6 border-t border-gray-700 flex justify-between">
<button type="button"
onClick={onClose}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-gray-300 font-medium transition-colors"
>
Close
</button>
<button type="button"
onClick={onMigrationsApplied}
className="px-4 py-2 bg-purple-500/20 hover:bg-purple-500/30 border border-purple-500/50 rounded-lg text-purple-400 font-medium transition-colors"
>
Refresh Status
</button>
</div>
</motion.div>
</div>
</AnimatePresence>
);
}
Loading