From 1dff0cb953f3d27fbe95e18e9e1c17644d4e4705 Mon Sep 17 00:00:00 2001 From: Cole Medin Date: Sat, 20 Sep 2025 12:25:54 -0500 Subject: [PATCH] Migrations and version APIs initial --- ...ion-and-migration-system-implementation.md | 622 ++++++++++++++++++ PRPs/version-and-migration-system.md | 342 ++++++++++ .../components/MigrationStatusCard.tsx | 132 ++++ .../components/PendingMigrationsModal.tsx | 195 ++++++ .../migrations/hooks/useMigrationQueries.ts | 58 ++ .../migrations/services/migrationService.ts | 47 ++ .../settings/migrations/types/index.ts | 41 ++ .../version/components/UpdateBanner.tsx | 60 ++ .../version/components/VersionStatusCard.tsx | 98 +++ .../version/hooks/useVersionQueries.ts | 59 ++ .../version/services/versionService.ts | 49 ++ .../features/settings/version/types/index.ts | 35 + archon-ui-main/src/pages/SettingsPage.tsx | 35 + docker-compose.yml | 1 + .../0.1.0/005_add_migration_tracking.sql | 62 ++ migration/RESET_DB.sql | 11 +- migration/complete_setup.sql | 53 ++ python/src/server/api_routes/migration_api.py | 170 +++++ python/src/server/api_routes/version_api.py | 121 ++++ python/src/server/config/version.py | 11 + python/src/server/main.py | 4 + .../src/server/services/migration_service.py | 233 +++++++ python/src/server/services/version_service.py | 162 +++++ python/src/server/utils/semantic_version.py | 107 +++ 24 files changed, 2706 insertions(+), 2 deletions(-) create mode 100644 PRPs/version-and-migration-system-implementation.md create mode 100644 PRPs/version-and-migration-system.md create mode 100644 archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx create mode 100644 archon-ui-main/src/features/settings/migrations/components/PendingMigrationsModal.tsx create mode 100644 archon-ui-main/src/features/settings/migrations/hooks/useMigrationQueries.ts create mode 100644 archon-ui-main/src/features/settings/migrations/services/migrationService.ts create mode 100644 archon-ui-main/src/features/settings/migrations/types/index.ts create mode 100644 archon-ui-main/src/features/settings/version/components/UpdateBanner.tsx create mode 100644 archon-ui-main/src/features/settings/version/components/VersionStatusCard.tsx create mode 100644 archon-ui-main/src/features/settings/version/hooks/useVersionQueries.ts create mode 100644 archon-ui-main/src/features/settings/version/services/versionService.ts create mode 100644 archon-ui-main/src/features/settings/version/types/index.ts create mode 100644 migration/0.1.0/005_add_migration_tracking.sql create mode 100644 python/src/server/api_routes/migration_api.py create mode 100644 python/src/server/api_routes/version_api.py create mode 100644 python/src/server/config/version.py create mode 100644 python/src/server/services/migration_service.py create mode 100644 python/src/server/services/version_service.py create mode 100644 python/src/server/utils/semantic_version.py diff --git a/PRPs/version-and-migration-system-implementation.md b/PRPs/version-and-migration-system-implementation.md new file mode 100644 index 00000000..b2558e2d --- /dev/null +++ b/PRPs/version-and-migration-system-implementation.md @@ -0,0 +1,622 @@ +name: "Version Checking and Migration Tracking System Implementation PRP" +description: | + Complete implementation guide for Archon's version checking and migration tracking system with comprehensive research and patterns + +--- + +## Goal + +**Feature Goal**: Implement a comprehensive version checking and migration tracking system for Archon that enables users to stay up-to-date and manage database schema changes safely when upgrading. + +**Deliverable**: Backend API endpoints for version checking and migration status, frontend UI components integrated with settings page, self-bootstrapping migration system with tracking. + +**Success Definition**: Users can see when updates are available, view pending migrations with SQL content, copy migration SQL to run manually in Supabase, and track migration history - all with proper error handling and caching. + +## User Persona + +**Target User**: Archon administrators and self-hosting users + +**Use Case**: +1. Check for new Archon releases when visiting settings page +2. View pending database migrations before upgrading +3. Copy and apply migrations manually via Supabase SQL Editor +4. Track migration history to verify system state + +**User Journey**: +1. User visits Settings page → sees version status (current vs latest) +2. If update available → sees prominent alert with link to release notes +3. Checks migrations section → sees X pending migrations +4. Opens pending migrations modal → views SQL content for each +5. Copies SQL → runs in Supabase → migration self-records +6. Refreshes status → sees migration applied successfully + +**Pain Points Addressed**: +- No visibility into available updates +- Database schema changes break functionality after git pull +- No tracking of which migrations have been applied +- Manual migration process lacks guidance + +## Why + +- **Business value**: Reduces support burden by helping users stay current and manage upgrades properly +- **Integration**: Extends existing settings page with new capabilities following established patterns +- **Problems solved**: Version awareness, migration tracking, safe schema evolution during beta phase + +## What + +Implement version checking against GitHub releases API, database migration tracking with self-recording pattern, and intuitive UI for managing both. System must handle bootstrap case (no migrations table), cache GitHub API responses to avoid rate limits, and provide clear migration guidance for Supabase's manual SQL execution requirement. + +### Success Criteria + +- [ ] Version checking shows current vs latest with update availability +- [ ] Migration system tracks applied vs pending migrations accurately +- [ ] Bootstrap case handled automatically when migrations table doesn't exist +- [ ] GitHub API cached for 1 hour to avoid rate limits +- [ ] Migrations include self-recording SQL at the end +- [ ] UI provides clear copy buttons for migration SQL +- [ ] Error states handled gracefully with fallback behavior + +## All Needed Context + +### Context Completeness Check + +_This PRP includes comprehensive research findings, existing codebase patterns, external API documentation, and detailed implementation patterns needed for one-pass implementation success._ + +### Documentation & References + +```yaml +# MUST READ - Include these in your context window +- url: https://docs.github.com/en/rest/releases/releases#get-the-latest-release + why: GitHub API endpoint for fetching latest release with exact response schema + critical: Returns 404 when no releases exist yet - must handle gracefully + +- file: python/src/server/api_routes/progress_api.py + why: Pattern for API routes with ETag support and error handling + pattern: Router setup, ETag generation/checking, HTTPException handling + gotcha: ETags use generate_etag from utils, check_etag returns boolean + +- file: python/src/server/services/credential_service.py + why: Service layer pattern with Supabase client initialization + pattern: Service class with async methods, _get_supabase_client pattern + gotcha: Must handle Supabase connection failures gracefully + +- file: archon-ui-main/src/features/projects/hooks/useProjectQueries.ts + why: TanStack Query v5 patterns with query keys factory + pattern: Query keys factory, optimistic updates, smart polling usage + gotcha: Use DISABLED_QUERY_KEY for disabled queries, STALE_TIMES constants + +- file: archon-ui-main/src/features/projects/services/projectService.ts + why: Frontend service pattern using callAPIWithETag + pattern: Service object with async methods, error handling + gotcha: Always use callAPIWithETag for consistency + +- docfile: PRPs/ai_docs/QUERY_PATTERNS.md + why: Complete TanStack Query patterns and conventions + section: Query key factories, shared patterns usage + +- docfile: PRPs/ai_docs/ARCHITECTURE.md + why: System architecture and directory structure conventions + section: Backend structure, Frontend vertical slices +``` + +### Current Codebase tree + +```bash +python/ +├── src/ +│ └── server/ +│ ├── api_routes/ +│ │ ├── settings_api.py +│ │ ├── progress_api.py +│ │ └── projects_api.py +│ ├── services/ +│ │ ├── credential_service.py +│ │ └── source_management_service.py +│ ├── config/ +│ │ ├── config.py +│ │ └── logfire_config.py +│ ├── utils/ +│ │ └── etag_utils.py +│ └── main.py + +archon-ui-main/ +├── src/ +│ ├── pages/ +│ │ └── SettingsPage.tsx +│ ├── features/ +│ │ ├── projects/ +│ │ │ ├── hooks/ +│ │ │ ├── services/ +│ │ │ └── types/ +│ │ └── shared/ +│ │ ├── apiWithEtag.ts +│ │ └── queryPatterns.ts +│ └── components/ +│ └── settings/ + +migration/ +├── 0.1.0/ +│ ├── 001_add_source_url_display_name.sql +│ ├── 002_add_hybrid_search_tsvector.sql +│ ├── 003_ollama_implementation.sql +│ └── 004_add_priority_column_to_tasks.sql +``` + +### Desired Codebase tree with files to be added + +```bash +python/ +├── src/ +│ └── server/ +│ ├── api_routes/ +│ │ ├── version_api.py # NEW: Version checking endpoints +│ │ └── migration_api.py # NEW: Migration tracking endpoints +│ ├── services/ +│ │ ├── version_service.py # NEW: GitHub API integration +│ │ └── migration_service.py # NEW: Migration scanning/tracking +│ ├── config/ +│ │ └── version.py # NEW: Version constant +│ └── utils/ +│ └── semantic_version.py # NEW: Version comparison utilities + +archon-ui-main/ +├── src/ +│ └── features/ +│ └── settings/ # NEW: Settings feature slice +│ ├── version/ # NEW: Version checking feature +│ │ ├── components/ +│ │ │ ├── VersionStatusCard.tsx +│ │ │ ├── UpdateBanner.tsx +│ │ │ └── UpgradeInstructionsModal.tsx +│ │ ├── hooks/ +│ │ │ └── useVersionQueries.ts +│ │ ├── services/ +│ │ │ └── versionService.ts +│ │ └── types/ +│ │ └── index.ts +│ └── migrations/ # NEW: Migration tracking feature +│ ├── components/ +│ │ ├── MigrationStatusCard.tsx +│ │ ├── PendingMigrationsModal.tsx +│ │ └── MigrationHistory.tsx +│ ├── hooks/ +│ │ └── useMigrationQueries.ts +│ ├── services/ +│ │ └── migrationService.ts +│ └── types/ +│ └── index.ts + +migration/ +├── 0.1.0/ +│ └── 005_add_migration_tracking.sql # NEW: Creates archon_migrations table +``` + +### Known Gotchas & Library Quirks + +```python +# CRITICAL: GitHub API returns 404 when repository has no releases +# Must handle this case and return update_available: false + +# CRITICAL: Supabase cannot execute SQL programmatically via SDK +# Users must manually copy and run migrations in Supabase SQL Editor + +# CRITICAL: Bootstrap case - migrations table may not exist on first run +# Migration service must check table existence before querying + +# CRITICAL: Frontend components in components/settings/ are legacy +# New features should go in features/settings/ following vertical slice + +# CRITICAL: All database tables must use archon_ prefix +# This is Supabase convention for application tables + +# CRITICAL: ETag checking returns True if client cache is stale +# Confusing but check_etag(if_none_match, current) returns True when NOT matching + +# CRITICAL: Use existing copyToClipboard utility for clipboard operations +# Import from features/shared/utils/clipboard.ts - handles fallbacks automatically +``` + +### GitHub API Response Format + +```json +// Exact response from GET /repos/{owner}/{repo}/releases/latest +{ + "url": "https://api.github.com/repos/coleam00/Archon/releases/123456789", + "html_url": "https://github.com/coleam00/Archon/releases/tag/v1.0.0", + "id": 217869415, + "author": { + "login": "coleam00", + "id": 102023614, + "avatar_url": "https://avatars.githubusercontent.com/u/102023614?v=4", + "html_url": "https://github.com/coleam00" + }, + "tag_name": "v1.0.0", + "target_commitish": "main", + "name": "Release v1.0.0", + "draft": false, + "prerelease": false, + "created_at": "2025-05-12T01:53:52Z", + "published_at": "2025-05-12T02:15:57Z", + "assets": [ + { + "id": 253814093, + "name": "archon-1.0.0-linux.AppImage", + "size": 171227028, + "download_count": 1249, + "browser_download_url": "https://github.com/coleam00/Archon/releases/download/v1.0.0/archon-1.0.0-linux.AppImage", + "content_type": "application/octet-stream" + } + ], + "tarball_url": "https://api.github.com/repos/coleam00/Archon/tarball/v1.0.0", + "zipball_url": "https://api.github.com/repos/coleam00/Archon/zipball/v1.0.0", + "body": "# Release Notes\n\n## What's Changed\n* Feature X by @user in #123\n* Bug fix Y by @user in #456\n\nFull changelog: https://github.com/coleam00/Archon/compare/v0.9.0...v1.0.0", + "reactions": { + "total_count": 89, + "+1": 31, + "heart": 17 + } +} + +// 404 Response when no releases exist: +{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/releases/releases#get-the-latest-release" +} +``` + +## Implementation Blueprint + +### Data models and structure + +```python +# python/src/server/config/version.py +ARCHON_VERSION = "0.1.0" # Update with each release + +# python/src/server/api_routes/version_api.py - Response models +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + +class VersionCheckResponse(BaseModel): + current: str + latest: Optional[str] + update_available: bool + release_url: Optional[str] + release_notes: Optional[str] + published_at: Optional[datetime] + check_error: Optional[str] = None + +# python/src/server/api_routes/migration_api.py - Response models +class MigrationRecord(BaseModel): + version: str + migration_name: str + applied_at: datetime + checksum: Optional[str] = None + +class PendingMigration(BaseModel): + version: str + name: str + sql_content: str + file_path: str + +class MigrationStatusResponse(BaseModel): + pending_migrations: list[PendingMigration] + applied_migrations: list[MigrationRecord] + has_pending: bool + bootstrap_required: bool = False +``` + +### Implementation Tasks (ordered by dependencies) + +```yaml +Task 1: CREATE python/src/server/config/version.py + - IMPLEMENT: ARCHON_VERSION constant = "0.1.0" + - NAMING: Simple string constant for now, can evolve to dataclass later + - PLACEMENT: Config directory with other configuration + +Task 2: CREATE migration/0.1.0/005_add_migration_tracking.sql + - IMPLEMENT: CREATE TABLE archon_migrations with self-recording INSERT + - FOLLOW pattern: Existing migrations in 0.1.0/ directory + - CRITICAL: Include ON CONFLICT DO NOTHING for idempotency + - PLACEMENT: Next sequential number in 0.1.0 directory + - EXACT SQL: + ```sql + CREATE TABLE IF NOT EXISTS archon_migrations ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + version VARCHAR(20) NOT NULL, + migration_name VARCHAR(255) NOT NULL, + applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(version, migration_name) + ); + + -- Index for fast lookups + CREATE INDEX IF NOT EXISTS idx_migrations_version ON archon_migrations(version); + + -- Record this migration as applied + INSERT INTO archon_migrations (version, migration_name) + VALUES ('0.1.0', '005_add_migration_tracking') + ON CONFLICT (version, migration_name) DO NOTHING; + ``` + +Task 3: CREATE python/src/server/services/migration_service.py + - IMPLEMENT: MigrationService class with filesystem scanning + - FOLLOW pattern: credential_service.py for Supabase client pattern + - NAMING: get_all_migrations(), get_applied_migrations(), get_pending_migrations() + - DEPENDENCIES: Needs Supabase client, filesystem access + - PLACEMENT: Services directory + +Task 4: CREATE python/src/server/services/version_service.py + - IMPLEMENT: VersionService with GitHub API integration and caching + - FOLLOW pattern: Service class with async methods + - NAMING: get_latest_version(), check_for_updates() + - DEPENDENCIES: httpx for GitHub API, in-memory cache with TTL + - PLACEMENT: Services directory + +Task 5: CREATE python/src/server/api_routes/version_api.py + - IMPLEMENT: FastAPI router with /check endpoint + - FOLLOW pattern: progress_api.py for router setup and ETag support + - NAMING: GET /api/version/check + - DEPENDENCIES: Import version_service, ETag utilities + - PLACEMENT: API routes directory + +Task 6: CREATE python/src/server/api_routes/migration_api.py + - IMPLEMENT: FastAPI router with /status and /history endpoints + - FOLLOW pattern: projects_api.py for router and response patterns + - NAMING: GET /api/migrations/status, GET /api/migrations/history + - DEPENDENCIES: Import migration_service + - PLACEMENT: API routes directory + +Task 7: MODIFY python/src/server/main.py + - INTEGRATE: Register new routers (version_api, migration_api) + - FIND pattern: Existing router imports and registrations + - ADD: from .api_routes import version_api, migration_api + - ADD: app.include_router(version_api.router), app.include_router(migration_api.router) + - PRESERVE: Existing router registrations + +Task 8: CREATE archon-ui-main/src/features/settings/version/types/index.ts + - IMPLEMENT: TypeScript types matching backend response models + - FOLLOW pattern: projects/types/index.ts for type definitions + - NAMING: VersionCheckResponse, VersionStatus interfaces + - PLACEMENT: New feature slice under features/settings + +Task 9: CREATE archon-ui-main/src/features/settings/version/services/versionService.ts + - IMPLEMENT: Service object with API methods + - FOLLOW pattern: projectService.ts using callAPIWithETag + - NAMING: checkVersion() async method + - DEPENDENCIES: Import callAPIWithETag from shared + - PLACEMENT: Version feature services directory + +Task 10: CREATE archon-ui-main/src/features/settings/version/hooks/useVersionQueries.ts + - IMPLEMENT: TanStack Query hooks with query keys factory + - FOLLOW pattern: useProjectQueries.ts for query patterns + - NAMING: versionKeys factory, useVersionCheck() hook + - DEPENDENCIES: Import from @tanstack/react-query, shared patterns + - PLACEMENT: Version feature hooks directory + +Task 11: CREATE archon-ui-main/src/features/settings/version/components/UpdateBanner.tsx + - IMPLEMENT: Banner component showing update availability + - FOLLOW pattern: Existing UI components with Tailwind styling + - NAMING: UpdateBanner component + - DEPENDENCIES: Use version hooks, Lucide icons + - PLACEMENT: Version feature components directory + +Task 12: CREATE archon-ui-main/src/features/settings/migrations/types/index.ts + - IMPLEMENT: Migration types (MigrationRecord, PendingMigration) + - FOLLOW pattern: Type definition patterns + - PLACEMENT: Migrations feature types directory + +Task 13: CREATE archon-ui-main/src/features/settings/migrations/services/migrationService.ts + - IMPLEMENT: API service for migration endpoints + - FOLLOW pattern: Service object pattern + - NAMING: getMigrationStatus(), getMigrationHistory() + - PLACEMENT: Migrations feature services directory + +Task 14: CREATE archon-ui-main/src/features/settings/migrations/hooks/useMigrationQueries.ts + - IMPLEMENT: Migration query hooks + - FOLLOW pattern: Query hooks with smart polling + - NAMING: migrationKeys factory, useMigrationStatus() + - PLACEMENT: Migrations feature hooks directory + +Task 15: CREATE archon-ui-main/src/features/settings/migrations/components/PendingMigrationsModal.tsx + - IMPLEMENT: Modal showing pending migrations with copy buttons + - FOLLOW pattern: Modal components with framer-motion + - CRITICAL: Use copyToClipboard from features/shared/utils/clipboard.ts + - REFERENCE: See ProjectCardActions.tsx for usage pattern with showToast + - PLACEMENT: Migrations feature components directory + +Task 16: MODIFY archon-ui-main/src/pages/SettingsPage.tsx + - INTEGRATE: Import and render new version/migration components + - ADD: UpdateBanner at top if update available + - ADD: Migration status section with pending count + - PRESERVE: Existing settings sections +``` + +### Implementation Patterns & Key Details + +```python +# Version Service Pattern - Caching with TTL +class VersionService: + def __init__(self): + self._cache: dict | None = None + self._cache_time: datetime | None = None + self._cache_ttl = 3600 # 1 hour + + async def get_latest_version(self) -> dict: + # PATTERN: Check cache first (follow credential_service.py caching) + if self._is_cache_valid(): + return self._cache + + # GOTCHA: GitHub API returns 404 for repos with no releases + try: + async with httpx.AsyncClient(timeout=1.0) as client: + response = await client.get( + "https://api.github.com/repos/coleam00/Archon/releases/latest", + headers={"Accept": "application/vnd.github.v3+json"} + ) + if response.status_code == 404: + # No releases yet + return {"update_available": False, "latest": None} + response.raise_for_status() + data = response.json() + self._cache = data + self._cache_time = datetime.now() + return data + except Exception as e: + # CRITICAL: Return cached data or safe default on failure + if self._cache: + return self._cache + return {"error": str(e), "update_available": False} + +# Migration Service Pattern - Bootstrap handling +class MigrationService: + async def check_migrations_table_exists(self) -> bool: + # PATTERN: Check table existence before querying + supabase = self._get_supabase_client() + try: + # Query information_schema to check if table exists + result = supabase.rpc( + "check_table_exists", + {"table_name": "archon_migrations"} + ).execute() + return result.data + except: + # Assume table doesn't exist if query fails + return False + + async def get_pending_migrations(self) -> list[PendingMigration]: + # CRITICAL: Check table exists first + if not await self.check_migrations_table_exists(): + # Bootstrap case - all migrations are pending + return await self.get_all_filesystem_migrations() + + # Normal case - compare filesystem vs database + all_migrations = await self.scan_migration_directory() + applied = await self.get_applied_migrations() + # ... comparison logic + +# Frontend Pattern - Smart Polling with Visibility +export function useVersionCheck() { + const { refetchInterval } = useSmartPolling(30000); // 30 seconds base + + return useQuery({ + queryKey: versionKeys.check(), + queryFn: () => versionService.checkVersion(), + staleTime: STALE_TIMES.rare, // 5 minutes + refetchInterval, // Pauses when tab hidden + retry: false, // Don't retry on 404 + }); +} +``` + +### Integration Points + +```yaml +DATABASE: + - migration: "005_add_migration_tracking.sql creates archon_migrations table" + - index: "CREATE INDEX idx_archon_migrations_version ON archon_migrations(version)" + +CONFIG: + - add to: python/src/server/config/version.py + - pattern: 'ARCHON_VERSION = "0.1.0"' + +ROUTES: + - add to: python/src/server/main.py + - pattern: | + from .api_routes import version_api, migration_api + app.include_router(version_api.router) + app.include_router(migration_api.router) + +FRONTEND: + - modify: archon-ui-main/src/pages/SettingsPage.tsx + - imports: "import { UpdateBanner } from '../features/settings/version/components'" + - render: "Add component at top of page" +``` + +## Validation Loop + +### Level 1: Syntax & Style (Immediate Feedback) + +```bash +# Backend validation +cd python +ruff check src/server/api_routes/version_api.py src/server/api_routes/migration_api.py --fix +ruff check src/server/services/version_service.py src/server/services/migration_service.py --fix +mypy src/server/api_routes/ src/server/services/ + +# Frontend validation +cd archon-ui-main +npm run biome:fix src/features/settings/ +npx tsc --noEmit 2>&1 | grep "src/features/settings" + +# Expected: Zero errors. Fix any issues before proceeding. +``` + +### Level 2: Unit Tests (Component Validation) + +```bash +# Backend tests to create +cd python + +# Test version service: +# - Test GitHub API 404 handling returns update_available: false +# - Test cache TTL expiration and refresh +# - Test version comparison logic (0.1.0 vs 1.0.0, v-prefix handling) +# - Test fallback to cached data on network failure +uv run pytest tests/server/services/test_version_service.py -v + +# Test migration service: +# - Test filesystem scanning finds all .sql files +# - Test bootstrap case when table doesn't exist +# - Test pending vs applied migration comparison +# - Test checksum calculation for migration files +uv run pytest tests/server/services/test_migration_service.py -v +``` + +## Final Validation Checklist + +### Technical Validation + +- [ ] Both validation levels completed successfully +- [ ] Backend tests pass: `uv run pytest tests/ -v` +- [ ] No linting errors: `ruff check src/` +- [ ] No type errors: `mypy src/` +- [ ] Frontend builds: `npm run build` + +### Feature Validation + +- [ ] Version check shows current vs latest correctly +- [ ] Update banner appears when new version available +- [ ] Migration status shows accurate pending count +- [ ] Pending migrations modal displays SQL content +- [ ] Copy buttons work for migration SQL +- [ ] Refresh updates migration status after applying +- [ ] Bootstrap case creates migrations table +- [ ] GitHub API failures handled gracefully + +### Code Quality Validation + +- [ ] Follows existing FastAPI patterns for routes +- [ ] Uses service layer pattern consistently +- [ ] Frontend follows vertical slice architecture +- [ ] TanStack Query patterns properly implemented +- [ ] Error handling matches existing patterns +- [ ] ETag support integrated where appropriate + +### Documentation & Deployment + +- [ ] Version constant documented for release process +- [ ] Migration self-recording pattern documented +- [ ] Environment variables documented if added +- [ ] CONTRIBUTING.md updated with migration guidelines + +--- + +## Anti-Patterns to Avoid + +- ❌ Don't try to execute migrations programmatically (Supabase limitation) +- ❌ Don't poll GitHub API without caching (rate limits) +- ❌ Don't assume migrations table exists (bootstrap case) +- ❌ Don't hardcode version strings outside of config/version.py +- ❌ Don't skip self-recording INSERT in migrations +- ❌ Don't use synchronous HTTP calls in async functions +- ❌ Don't put new UI components in legacy components/ directory \ No newline at end of file diff --git a/PRPs/version-and-migration-system.md b/PRPs/version-and-migration-system.md new file mode 100644 index 00000000..e89dff03 --- /dev/null +++ b/PRPs/version-and-migration-system.md @@ -0,0 +1,342 @@ +# Version Checking and Migration Tracking System Implementation + +## Overview +Implement a comprehensive version checking and migration tracking system for Archon to help users stay up-to-date and manage database schema changes when upgrading. + +## 1. Version Checking API + +### Backend Implementation + +#### 1.1 Create Version Check Endpoint +**File**: `python/src/server/api_routes/version_api.py` + +Create a new API router with the following endpoint: + +```python +GET /api/version/check +``` + +**Implementation Requirements:** +- Call GitHub API: `https://api.github.com/repos/coleam00/Archon/releases/latest` +- Handle case where no releases exist yet (404 response) - return `update_available: false` +- Compare with current Archon version (stored in a constants file that you will need to create too) +- Return response format: + ```json + { + "current": "1.0.0", + "latest": "1.1.0", + "update_available": true, + "release_url": "https://github.com/coleam00/Archon/releases/tag/v1.1.0", + "release_notes": "Release notes from GitHub...", + "published_at": "2024-01-15T10:00:00Z" + } + ``` +- Use 1-second timeout for GitHub API call +- Cache response for 1 hour to avoid rate limiting (use simple in-memory cache) +- Graceful fallback: return current version as latest on error or if no releases exist +- Add environment variable `ARCHON_VERSION_CHECK_ENABLED` (default: `true`) +- If disabled, on error, or no releases exist, return `update_available: false` + +#### 1.2 Version Configuration +**File**: `python/src/server/config/version.py` + +Create version configuration: +```python +ARCHON_VERSION = "1.0.0" # Update this with each release +``` + +### Frontend Implementation + +#### 1.3 Version Query Hook +**File**: `archon-ui-main/src/features/settings/hooks/useVersionQueries.ts` + +Create TanStack Query hooks: +- `useVersionCheck()` - Query version endpoint +- Use `STALE_TIMES.rare` (5 minutes) for caching +- Only check when settings page is open + +#### 1.4 Version Service +**File**: `archon-ui-main/src/features/settings/services/versionService.ts` + +Create service for API calls: +```typescript +export const versionService = { + async checkVersion(): Promise { + // Call /api/version/check + } +} +``` + +#### 1.5 Update Banner Component +**File**: `archon-ui-main/src/features/settings/components/UpdateBanner.tsx` + +Create update notification banner: +- Show at top of settings page when update available +- Display current vs latest version +- Link to GitHub release page +- Include "View Upgrade Instructions" button that opens modal + +#### 1.6 Upgrade Instructions Modal +**File**: `archon-ui-main/src/features/settings/components/UpgradeInstructionsModal.tsx` + +Show upgrade instructions: +- Display standard upgrade steps: + 1. Pull latest changes: `git pull` + 2. Check for pending migrations (link to migrations section) + 3. Rebuild and restart: `docker compose up -d --build` +- Link to GitHub release notes +- Warning about checking migrations before upgrading + +## 2. Migration Tracking System + +### Database Schema + +#### 2.1 Create Migrations Table +**File**: Add to next migration SQL file (e.g., `migration/0.1.0/005_add_migration_tracking.sql`) + +```sql +CREATE TABLE IF NOT EXISTS archon_migrations ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + version VARCHAR(20) NOT NULL, + migration_name VARCHAR(255) NOT NULL, + applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(version, migration_name) +); + +-- Index for fast lookups +CREATE INDEX idx_migrations_version ON archon_migrations(version); + +-- Record this migration as applied +INSERT INTO archon_migrations (version, migration_name) +VALUES ('0.1.0', '005_add_migration_tracking') +ON CONFLICT (version, migration_name) DO NOTHING; +``` + +### Migration File Structure + +#### 2.2 Migration Organization +**Directory Structure (Example, not actual versions we have)**: +``` +migration/ +├── 1.0.0/ +│ └── 001_initial_schema.sql +├── 1.1.0/ +│ ├── 001_add_migration_tracking.sql +│ └── 002_add_new_feature.sql +└── 1.2.0/ + └── 001_update_columns.sql +``` + +**Important**: Each migration SQL file must include an INSERT statement at the end to record itself in the `archon_migrations` table (see example in 2.1). + +### Backend Implementation + +#### 2.4 Migration Service +**File**: `python/src/server/services/migration_service.py` + +Implement migration tracking: +- `get_all_migrations()` - Scan `migration/` folder for all SQL files +- `get_applied_migrations()` - Query archon_migrations table (handle case where table doesn't exist) +- `get_pending_migrations()` - Compare filesystem vs database +- `check_migrations_table_exists()` - Check if archon_migrations table exists (bootstrap case) + +#### 2.5 Migration API Endpoints +**File**: `python/src/server/api_routes/migration_api.py` + +Create endpoints: + +```python +GET /api/migrations/status +``` +Returns: +```json +{ + "pending_migrations": [ + { + "version": "1.1.0", + "name": "001_add_new_table", + "sql_content": "CREATE TABLE...", + "file_path": "1.1.0/001_add_new_table.sql" + } + ], + "applied_migrations": [ + { + "version": "1.0.0", + "name": "001_initial_schema", + "applied_at": "2024-01-01T10:00:00Z" + } + ], + "has_pending": true +} +``` + +```python +GET /api/migrations/history +``` +Returns list of all applied migrations with timestamps + +### Frontend Implementation + +#### 2.6 Migration Query Hooks +**File**: `archon-ui-main/src/features/settings/hooks/useMigrationQueries.ts` + +Create hooks: +- `useMigrationStatus()` - Get pending/applied migrations +- `useMigrationHistory()` - Get migration history + +#### 2.7 Migration Service +**File**: `archon-ui-main/src/features/settings/services/migrationService.ts` + +API service methods for migration endpoints + +#### 2.8 Pending Migrations Modal +**File**: `archon-ui-main/src/features/settings/components/PendingMigrationsModal.tsx` + +Display pending migrations: +- Show alert badge on settings page when migrations pending +- Modal with list of pending migrations +- Each migration shows: + - Version number + - Migration name + - Expandable SQL content (with copy button) +- Instructions: + 1. Copy the SQL script + 2. Go to Supabase SQL Editor + 3. Run the script (which will automatically record it as applied) + 4. Click "Refresh Status" to update the UI +- "Refresh Status" button to re-query migration status +- Auto-refresh status every few seconds while modal is open + +#### 2.9 Migration Settings Section +**File**: Update existing settings page + +Add migrations section showing: +- Current migration status (X pending, Y applied) +- "View Pending Migrations" button (opens modal) +- "View Migration History" link +- Last migration applied timestamp + +## 3. Integration Points + +### 3.1 Settings Page Updates +**File**: `archon-ui-main/src/pages/SettingsPage.tsx` + +Add new sections: +- Version information section with update banner +- Migration status section with pending count + +### 3.2 App Initialization Check +**File**: `archon-ui-main/src/App.tsx` or main layout + +On app load: +- Check for pending migrations +- If migrations pending, show non-dismissible alert banner +- Banner links to settings page migrations section + +## 4. Environment Variables + +Add to `.env.example`: +```bash +# Version checking (optional) +ARCHON_VERSION_CHECK_ENABLED=true # Set to false to disable version checking +``` + +## 5. Testing Requirements + +### Backend Tests +- Mock GitHub API responses (research exact API response format for this) +- Test version comparison logic +- Test migration file scanning +- Test migration status comparison + +### Frontend Tests +- Test version check display +- Test migration modal functionality +- Test migration status refresh workflow + +## 6. Implementation Order + +1. **Phase 1 - Version Checking**: + - Backend version API + - Frontend version checking components + - Settings page integration + +2. **Phase 2 - Migration Infrastructure**: + - Database table creation + - Migration file structure + +3. **Phase 3 - Migration Tracking**: + - Backend migration service and API + - Frontend migration components + - Settings page migration section + +4. **Phase 4 - Polish**: + - App initialization checks + - Alert banners + - Testing + +## 7. Future Enhancements (Not for Initial Implementation) + +- Automatic migration runner for PostgreSQL (when we move from Supabase) +- Migration rollback tracking +- Version-specific upgrade guides +- Pre-upgrade validation checks +- Migration dry-run capability + +## Notes for Implementation + +### Important Constraints +1. **Supabase Limitation**: We cannot run SQL migrations programmatically due to Supabase SDK restrictions. Users must manually run SQL in Supabase SQL Editor. +2. **Simplicity**: Keep UI simple and focused. No unnecessary toasts or complex workflows. +3. **Beta Consideration**: Since Archon is in beta, version checking should be enabled by default to help users stay current. + +### Key Design Decisions +1. **Direct GitHub API**: Use GitHub releases API directly instead of maintaining separate version service +2. **Self-Recording Migrations**: Each migration SQL includes INSERT to record itself when run +3. **Bootstrap Handling**: If migrations table doesn't exist, that's the first pending migration +4. **Simple Tracking**: If migration is in database, it's been run. No complex status tracking. +5. **Non-intrusive Updates**: Version checks only on settings page, not constant polling + +### Success Criteria +- Users can see when new version is available +- Users can easily see pending migrations +- Users can copy migration SQL which self-records when run +- System tracks migration history accurately +- Handles bootstrap case when migrations table doesn't exist +- Minimal performance impact on app startup + +## 8. Contributing.md Addition + +**File**: Add to `CONTRIBUTING.md` + +Add the following section: + +### Creating Database Migrations + +When adding database schema changes: + +1. Create a new migration file in the appropriate version folder: + ``` + migration/{version}/XXX_description.sql + ``` + Where XXX is a three-digit number (e.g., 001, 002, 003) + +2. **IMPORTANT**: Every migration must end with an INSERT statement to record itself: + ```sql + -- Your migration SQL here + CREATE TABLE example_table ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL + ); + + -- REQUIRED: Record this migration + INSERT INTO archon_migrations (version, migration_name) + VALUES ('{version}', 'XXX_description') + ON CONFLICT (version, migration_name) DO NOTHING; + ``` + +3. The INSERT ensures the migration is tracked after successful execution +4. Use `ON CONFLICT DO NOTHING` to make migrations idempotent +5. Replace `{version}` with the actual version number (e.g., '0.1.0') + Replace `XXX_description` with the actual migration filename (e.g., '001_add_new_table') +6. Migration names should be descriptive but concise \ No newline at end of file diff --git a/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx b/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx new file mode 100644 index 00000000..2b29531c --- /dev/null +++ b/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx @@ -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 ( + <> + +
+
+ +

Database Migrations

+
+ +
+ +
+
+ Applied Migrations + {data?.applied_count ?? 0} +
+ +
+ Pending Migrations +
+ {data?.pending_count ?? 0} + {data && data.pending_count > 0 && } +
+
+ +
+ Status +
+ {isLoading ? ( + <> + + Checking... + + ) : error ? ( + <> + + Error loading + + ) : data?.bootstrap_required ? ( + <> + + Setup required + + ) : data?.has_pending ? ( + <> + + Migrations pending + + ) : ( + <> + + Up to date + + )} +
+
+ + {data?.current_version && ( +
+ Database Version + {data.current_version} +
+ )} +
+ + {data?.has_pending && ( +
+

+ {data.bootstrap_required + ? "Initial database setup is required." + : `${data.pending_count} migration${data.pending_count > 1 ? "s" : ""} need to be applied.`} +

+ +
+ )} + + {error && ( +
+

+ Failed to load migration status. Please check your database connection. +

+
+ )} +
+ + {/* Modal for viewing pending migrations */} + {data && ( + setIsModalOpen(false)} + migrations={data.pending_migrations} + onMigrationsApplied={refetch} + /> + )} + + ); +} diff --git a/archon-ui-main/src/features/settings/migrations/components/PendingMigrationsModal.tsx b/archon-ui-main/src/features/settings/migrations/components/PendingMigrationsModal.tsx new file mode 100644 index 00000000..f4bd23c0 --- /dev/null +++ b/archon-ui-main/src/features/settings/migrations/components/PendingMigrationsModal.tsx @@ -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(null); + const [expandedIndex, setExpandedIndex] = React.useState(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 ( + +
+ {/* Backdrop */} + + + {/* Modal */} + + {/* Header */} +
+
+ +

Pending Database Migrations

+
+ +
+ + {/* Instructions */} +
+

+ + How to Apply Migrations +

+
    +
  1. Copy the SQL for each migration below
  2. +
  3. Open your Supabase dashboard SQL Editor
  4. +
  5. Paste and execute each migration in order
  6. +
  7. Click "Refresh Status" below to verify migrations were applied
  8. +
+ {migrations.length > 1 && ( + + )} +
+ + {/* Migration List */} +
+ {migrations.length === 0 ? ( +
+ +

All migrations have been applied!

+
+ ) : ( +
+ {migrations.map((migration, index) => ( +
+
+
+
+

{migration.name}

+

+ Version: {migration.version} • {migration.file_path} +

+
+
+ + +
+
+ + {/* SQL Content */} + + {expandedIndex === index && ( + +
+                              {migration.sql_content}
+                            
+
+ )} +
+
+
+ ))} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/archon-ui-main/src/features/settings/migrations/hooks/useMigrationQueries.ts b/archon-ui-main/src/features/settings/migrations/hooks/useMigrationQueries.ts new file mode 100644 index 00000000..1c2a6d7e --- /dev/null +++ b/archon-ui-main/src/features/settings/migrations/hooks/useMigrationQueries.ts @@ -0,0 +1,58 @@ +/** + * TanStack Query hooks for migration tracking + */ + +import { useQuery } from "@tanstack/react-query"; +import { STALE_TIMES } from "@/features/shared/queryPatterns"; +import { useSmartPolling } from "@/features/ui/hooks/useSmartPolling"; +import { migrationService } from "../services/migrationService"; +import type { MigrationHistoryResponse, MigrationStatusResponse, PendingMigration } from "../types"; + +// Query key factory +export const migrationKeys = { + all: ["migrations"] as const, + status: () => [...migrationKeys.all, "status"] as const, + history: () => [...migrationKeys.all, "history"] as const, + pending: () => [...migrationKeys.all, "pending"] as const, +}; + +/** + * Hook to get comprehensive migration status + * Polls more frequently when migrations are pending + */ +export function useMigrationStatus() { + // Poll every 30 seconds when tab is visible + const { refetchInterval } = useSmartPolling(30000); + + return useQuery({ + queryKey: migrationKeys.status(), + queryFn: () => migrationService.getMigrationStatus(), + staleTime: STALE_TIMES.normal, // 30 seconds + refetchInterval, + }); +} + +/** + * Hook to get migration history + */ +export function useMigrationHistory() { + return useQuery({ + queryKey: migrationKeys.history(), + queryFn: () => migrationService.getMigrationHistory(), + staleTime: STALE_TIMES.rare, // 5 minutes - history doesn't change often + }); +} + +/** + * Hook to get pending migrations only + */ +export function usePendingMigrations() { + const { refetchInterval } = useSmartPolling(30000); + + return useQuery({ + queryKey: migrationKeys.pending(), + queryFn: () => migrationService.getPendingMigrations(), + staleTime: STALE_TIMES.normal, + refetchInterval, + }); +} diff --git a/archon-ui-main/src/features/settings/migrations/services/migrationService.ts b/archon-ui-main/src/features/settings/migrations/services/migrationService.ts new file mode 100644 index 00000000..93cb15eb --- /dev/null +++ b/archon-ui-main/src/features/settings/migrations/services/migrationService.ts @@ -0,0 +1,47 @@ +/** + * Service for database migration tracking and management + */ + +import { callAPIWithETag } from "@/features/shared/apiWithEtag"; +import type { MigrationHistoryResponse, MigrationStatusResponse, PendingMigration } from "../types"; + +export const migrationService = { + /** + * Get comprehensive migration status including pending and applied + */ + async getMigrationStatus(): Promise { + try { + const response = await callAPIWithETag("/api/migrations/status"); + return response as MigrationStatusResponse; + } catch (error) { + console.error("Error getting migration status:", error); + throw error; + } + }, + + /** + * Get history of applied migrations + */ + async getMigrationHistory(): Promise { + try { + const response = await callAPIWithETag("/api/migrations/history"); + return response as MigrationHistoryResponse; + } catch (error) { + console.error("Error getting migration history:", error); + throw error; + } + }, + + /** + * Get list of pending migrations only + */ + async getPendingMigrations(): Promise { + try { + const response = await callAPIWithETag("/api/migrations/pending"); + return response as PendingMigration[]; + } catch (error) { + console.error("Error getting pending migrations:", error); + throw error; + } + }, +}; diff --git a/archon-ui-main/src/features/settings/migrations/types/index.ts b/archon-ui-main/src/features/settings/migrations/types/index.ts new file mode 100644 index 00000000..7c08c6bf --- /dev/null +++ b/archon-ui-main/src/features/settings/migrations/types/index.ts @@ -0,0 +1,41 @@ +/** + * Type definitions for database migration tracking and management + */ + +export interface MigrationRecord { + version: string; + migration_name: string; + applied_at: string; + checksum?: string | null; +} + +export interface PendingMigration { + version: string; + name: string; + sql_content: string; + file_path: string; + checksum?: string | null; +} + +export interface MigrationStatusResponse { + pending_migrations: PendingMigration[]; + applied_migrations: MigrationRecord[]; + has_pending: boolean; + bootstrap_required: boolean; + current_version: string; + pending_count: number; + applied_count: number; +} + +export interface MigrationHistoryResponse { + migrations: MigrationRecord[]; + total_count: number; + current_version: string; +} + +export interface MigrationState { + status: MigrationStatusResponse | null; + isLoading: boolean; + error: Error | null; + selectedMigration: PendingMigration | null; +} diff --git a/archon-ui-main/src/features/settings/version/components/UpdateBanner.tsx b/archon-ui-main/src/features/settings/version/components/UpdateBanner.tsx new file mode 100644 index 00000000..ead37b2d --- /dev/null +++ b/archon-ui-main/src/features/settings/version/components/UpdateBanner.tsx @@ -0,0 +1,60 @@ +/** + * Banner component that shows when an update is available + */ + +import { AnimatePresence, motion } from "framer-motion"; +import { ArrowUpCircle, ExternalLink, X } from "lucide-react"; +import React from "react"; +import { useVersionCheck } from "../hooks/useVersionQueries"; + +export function UpdateBanner() { + const { data, isLoading, error } = useVersionCheck(); + const [isDismissed, setIsDismissed] = React.useState(false); + + // Don't show banner if loading, error, no data, or no update available + if (isLoading || error || !data?.update_available || isDismissed) { + return null; + } + + return ( + + +
+
+ +
+

Update Available: v{data.latest}

+

You are currently running v{data.current}

+
+
+
+ {data.release_url && ( + + View Release + + + )} + +
+
+
+
+ ); +} diff --git a/archon-ui-main/src/features/settings/version/components/VersionStatusCard.tsx b/archon-ui-main/src/features/settings/version/components/VersionStatusCard.tsx new file mode 100644 index 00000000..250b85d1 --- /dev/null +++ b/archon-ui-main/src/features/settings/version/components/VersionStatusCard.tsx @@ -0,0 +1,98 @@ +/** + * Card component showing current version status + */ + +import { motion } from "framer-motion"; +import { AlertCircle, CheckCircle, Info, RefreshCw } from "lucide-react"; +import { useClearVersionCache, useVersionCheck } from "../hooks/useVersionQueries"; + +export function VersionStatusCard() { + const { data, isLoading, error, refetch } = useVersionCheck(); + const clearCache = useClearVersionCache(); + + const handleRefreshClick = async () => { + // Clear cache and then refetch + await clearCache.mutateAsync(); + refetch(); + }; + + return ( + +
+
+ +

Version Information

+
+ +
+ +
+
+ Current Version + {data?.current || "Loading..."} +
+ +
+ Latest Version + + {isLoading ? "Checking..." : error ? "Check failed" : data?.latest ? data.latest : "No releases found"} + +
+ +
+ Status +
+ {isLoading ? ( + <> + + Checking... + + ) : error ? ( + <> + + Error checking + + ) : data?.update_available ? ( + <> + + Update available + + ) : ( + <> + + Up to date + + )} +
+
+ + {data?.published_at && ( +
+ Released + {new Date(data.published_at).toLocaleDateString()} +
+ )} +
+ + {error && ( +
+

+ {data?.check_error || "Failed to check for updates. Please try again later."} +

+
+ )} +
+ ); +} diff --git a/archon-ui-main/src/features/settings/version/hooks/useVersionQueries.ts b/archon-ui-main/src/features/settings/version/hooks/useVersionQueries.ts new file mode 100644 index 00000000..e1aefbd8 --- /dev/null +++ b/archon-ui-main/src/features/settings/version/hooks/useVersionQueries.ts @@ -0,0 +1,59 @@ +/** + * TanStack Query hooks for version checking + */ + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { STALE_TIMES } from "@/features/shared/queryPatterns"; +import { useSmartPolling } from "@/features/ui/hooks/useSmartPolling"; +import { versionService } from "../services/versionService"; +import type { VersionCheckResponse } from "../types"; + +// Query key factory +export const versionKeys = { + all: ["version"] as const, + check: () => [...versionKeys.all, "check"] as const, + current: () => [...versionKeys.all, "current"] as const, +}; + +/** + * Hook to check for version updates + * Polls every 5 minutes when tab is visible + */ +export function useVersionCheck() { + // Smart polling: check every 5 minutes when tab is visible + const { refetchInterval } = useSmartPolling(300000); // 5 minutes + + return useQuery({ + queryKey: versionKeys.check(), + queryFn: () => versionService.checkVersion(), + staleTime: STALE_TIMES.rare, // 5 minutes + refetchInterval, + retry: false, // Don't retry on 404 or network errors + }); +} + +/** + * Hook to get current version without checking for updates + */ +export function useCurrentVersion() { + return useQuery({ + queryKey: versionKeys.current(), + queryFn: () => versionService.getCurrentVersion(), + staleTime: STALE_TIMES.static, // Never stale + }); +} + +/** + * Hook to clear version cache and force fresh check + */ +export function useClearVersionCache() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => versionService.clearCache(), + onSuccess: () => { + // Invalidate version queries to force fresh check + queryClient.invalidateQueries({ queryKey: versionKeys.all }); + }, + }); +} diff --git a/archon-ui-main/src/features/settings/version/services/versionService.ts b/archon-ui-main/src/features/settings/version/services/versionService.ts new file mode 100644 index 00000000..4ef45b82 --- /dev/null +++ b/archon-ui-main/src/features/settings/version/services/versionService.ts @@ -0,0 +1,49 @@ +/** + * Service for version checking and update management + */ + +import { callAPIWithETag } from "@/features/shared/apiWithEtag"; +import type { CurrentVersionResponse, VersionCheckResponse } from "../types"; + +export const versionService = { + /** + * Check for available Archon updates + */ + async checkVersion(): Promise { + try { + const response = await callAPIWithETag("/api/version/check"); + return response as VersionCheckResponse; + } catch (error) { + console.error("Error checking version:", error); + throw error; + } + }, + + /** + * Get current Archon version without checking for updates + */ + async getCurrentVersion(): Promise { + try { + const response = await callAPIWithETag("/api/version/current"); + return response as CurrentVersionResponse; + } catch (error) { + console.error("Error getting current version:", error); + throw error; + } + }, + + /** + * Clear version cache to force fresh check + */ + async clearCache(): Promise<{ message: string; success: boolean }> { + try { + const response = await callAPIWithETag("/api/version/clear-cache", { + method: "POST", + }); + return response as { message: string; success: boolean }; + } catch (error) { + console.error("Error clearing version cache:", error); + throw error; + } + }, +}; diff --git a/archon-ui-main/src/features/settings/version/types/index.ts b/archon-ui-main/src/features/settings/version/types/index.ts new file mode 100644 index 00000000..04da0860 --- /dev/null +++ b/archon-ui-main/src/features/settings/version/types/index.ts @@ -0,0 +1,35 @@ +/** + * Type definitions for version checking and update management + */ + +export interface ReleaseAsset { + name: string; + size: number; + download_count: number; + browser_download_url: string; + content_type: string; +} + +export interface VersionCheckResponse { + current: string; + latest: string | null; + update_available: boolean; + release_url: string | null; + release_notes: string | null; + published_at: string | null; + check_error?: string | null; + assets?: ReleaseAsset[] | null; + author?: string | null; +} + +export interface CurrentVersionResponse { + version: string; + timestamp: string; +} + +export interface VersionStatus { + isLoading: boolean; + error: Error | null; + data: VersionCheckResponse | null; + lastChecked: Date | null; +} diff --git a/archon-ui-main/src/pages/SettingsPage.tsx b/archon-ui-main/src/pages/SettingsPage.tsx index ad186e87..20c3c412 100644 --- a/archon-ui-main/src/pages/SettingsPage.tsx +++ b/archon-ui-main/src/pages/SettingsPage.tsx @@ -10,6 +10,8 @@ import { Code, FileCode, Bug, + Info, + Database, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { useToast } from "../features/ui/hooks/useToast"; @@ -28,6 +30,9 @@ import { RagSettings, CodeExtractionSettings as CodeExtractionSettingsType, } from "../services/credentialsService"; +import { UpdateBanner } from "../features/settings/version/components/UpdateBanner"; +import { VersionStatusCard } from "../features/settings/version/components/VersionStatusCard"; +import { MigrationStatusCard } from "../features/settings/migrations/components/MigrationStatusCard"; export const SettingsPage = () => { const [ragSettings, setRagSettings] = useState({ @@ -106,6 +111,9 @@ export const SettingsPage = () => { variants={containerVariants} className="w-full" > + {/* Update Banner */} + + {/* Header */} { + + {/* Version Status */} + + + + + + + {/* Migration Status */} + + + + + + {projectsEnabled && ( str: + """Calculate MD5 checksum of migration content.""" + return hashlib.md5(content.encode()).hexdigest() + + +class MigrationService: + """Service for managing database migrations.""" + + def __init__(self): + self._supabase: Client | None = None + # Handle both Docker (/app/migration) and local (./migration) environments + if Path("/app/migration").exists(): + self._migrations_dir = Path("/app/migration") + else: + self._migrations_dir = Path("migration") + + def _get_supabase_client(self) -> Client: + """Get or create Supabase client.""" + if not self._supabase: + self._supabase = get_supabase_client() + return self._supabase + + async def check_migrations_table_exists(self) -> bool: + """ + Check if the archon_migrations table exists in the database. + + Returns: + True if table exists, False otherwise + """ + try: + supabase = self._get_supabase_client() + + # Query to check if table exists + result = supabase.rpc( + "sql", + { + "query": """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'archon_migrations' + ) as exists + """ + } + ).execute() + + # Check if result indicates table exists + if result.data and len(result.data) > 0: + return result.data[0].get("exists", False) + return False + except Exception: + # If the SQL function doesn't exist or query fails, try direct query + try: + supabase = self._get_supabase_client() + # Try to select from the table with limit 0 + supabase.table("archon_migrations").select("id").limit(0).execute() + return True + except Exception as e: + logfire.info(f"Migrations table does not exist: {e}") + return False + + async def get_applied_migrations(self) -> list[MigrationRecord]: + """ + Get list of applied migrations from the database. + + Returns: + List of MigrationRecord objects + """ + try: + # Check if table exists first + if not await self.check_migrations_table_exists(): + logfire.info("Migrations table does not exist, returning empty list") + return [] + + supabase = self._get_supabase_client() + result = supabase.table("archon_migrations").select("*").order("applied_at", desc=True).execute() + + return [MigrationRecord(row) for row in result.data] + except Exception as e: + logfire.error(f"Error fetching applied migrations: {e}") + # Return empty list if we can't fetch migrations + return [] + + async def scan_migration_directory(self) -> list[PendingMigration]: + """ + Scan the migration directory for all SQL files. + + Returns: + List of PendingMigration objects + """ + migrations = [] + + if not self._migrations_dir.exists(): + logfire.warning(f"Migration directory does not exist: {self._migrations_dir}") + return migrations + + # Scan all version directories + for version_dir in sorted(self._migrations_dir.iterdir()): + if not version_dir.is_dir(): + continue + + version = version_dir.name + + # Scan all SQL files in version directory + for sql_file in sorted(version_dir.glob("*.sql")): + try: + # Read SQL content + with open(sql_file, encoding="utf-8") as f: + sql_content = f.read() + + # Extract migration name (filename without extension) + migration_name = sql_file.stem + + # Create pending migration object + migration = PendingMigration( + version=version, + name=migration_name, + sql_content=sql_content, + file_path=str(sql_file.relative_to(Path.cwd())), + ) + migrations.append(migration) + except Exception as e: + logfire.error(f"Error reading migration file {sql_file}: {e}") + + return migrations + + async def get_pending_migrations(self) -> list[PendingMigration]: + """ + Get list of pending migrations by comparing filesystem with database. + + Returns: + List of PendingMigration objects that haven't been applied + """ + # Get all migrations from filesystem + all_migrations = await self.scan_migration_directory() + + # Check if migrations table exists + if not await self.check_migrations_table_exists(): + # Bootstrap case - all migrations are pending + logfire.info("Migrations table doesn't exist, all migrations are pending") + return all_migrations + + # Get applied migrations from database + applied_migrations = await self.get_applied_migrations() + + # Create set of applied migration identifiers + applied_set = {(m.version, m.migration_name) for m in applied_migrations} + + # Filter out applied migrations + pending = [m for m in all_migrations if (m.version, m.name) not in applied_set] + + return pending + + async def get_migration_status(self) -> dict[str, Any]: + """ + Get comprehensive migration status. + + Returns: + Dictionary with pending and applied migrations info + """ + pending = await self.get_pending_migrations() + applied = await self.get_applied_migrations() + + # Check if bootstrap is required + bootstrap_required = not await self.check_migrations_table_exists() + + return { + "pending_migrations": [ + { + "version": m.version, + "name": m.name, + "sql_content": m.sql_content, + "file_path": m.file_path, + "checksum": m.checksum, + } + for m in pending + ], + "applied_migrations": [ + { + "version": m.version, + "migration_name": m.migration_name, + "applied_at": m.applied_at, + "checksum": m.checksum, + } + for m in applied + ], + "has_pending": len(pending) > 0, + "bootstrap_required": bootstrap_required, + "current_version": ARCHON_VERSION, + "pending_count": len(pending), + "applied_count": len(applied), + } + + +# Export singleton instance +migration_service = MigrationService() diff --git a/python/src/server/services/version_service.py b/python/src/server/services/version_service.py new file mode 100644 index 00000000..b916c984 --- /dev/null +++ b/python/src/server/services/version_service.py @@ -0,0 +1,162 @@ +""" +Version checking service with GitHub API integration. +""" + +from datetime import datetime, timedelta +from typing import Any + +import httpx +import logfire + +from ..config.version import ARCHON_VERSION, GITHUB_REPO_NAME, GITHUB_REPO_OWNER +from ..utils.semantic_version import is_newer_version + + +class VersionService: + """Service for checking Archon version against GitHub releases.""" + + def __init__(self): + self._cache: dict[str, Any] | None = None + self._cache_time: datetime | None = None + self._cache_ttl = 3600 # 1 hour cache TTL + + def _is_cache_valid(self) -> bool: + """Check if cached data is still valid.""" + if not self._cache or not self._cache_time: + return False + + age = datetime.now() - self._cache_time + return age < timedelta(seconds=self._cache_ttl) + + async def get_latest_release(self) -> dict[str, Any] | None: + """ + Fetch latest release information from GitHub API. + + Returns: + Release data dictionary or None if no releases + """ + # Check cache first + if self._is_cache_valid(): + logfire.debug("Using cached version data") + return self._cache + + # GitHub API endpoint + url = f"https://api.github.com/repos/{GITHUB_REPO_OWNER}/{GITHUB_REPO_NAME}/releases/latest" + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get( + url, + headers={ + "Accept": "application/vnd.github.v3+json", + "User-Agent": f"Archon/{ARCHON_VERSION}", + }, + ) + + # Handle 404 - no releases yet + if response.status_code == 404: + logfire.info("No releases found on GitHub") + return None + + response.raise_for_status() + data = response.json() + + # Cache the successful response + self._cache = data + self._cache_time = datetime.now() + + return data + + except httpx.TimeoutException: + logfire.warning("GitHub API request timed out") + # Return cached data if available + if self._cache: + return self._cache + return None + except httpx.HTTPError as e: + logfire.error(f"HTTP error fetching latest release: {e}") + # Return cached data if available + if self._cache: + return self._cache + return None + except Exception as e: + logfire.error(f"Unexpected error fetching latest release: {e}") + # Return cached data if available + if self._cache: + return self._cache + return None + + async def check_for_updates(self) -> dict[str, Any]: + """ + Check if a newer version of Archon is available. + + Returns: + Dictionary with version check results + """ + try: + # Get latest release from GitHub + release = await self.get_latest_release() + + if not release: + # No releases found or error occurred + return { + "current": ARCHON_VERSION, + "latest": None, + "update_available": False, + "release_url": None, + "release_notes": None, + "published_at": None, + "check_error": None, + } + + # Extract version from tag_name (e.g., "v1.0.0" -> "1.0.0") + latest_version = release.get("tag_name", "") + if latest_version.startswith("v"): + latest_version = latest_version[1:] + + # Check if update is available + update_available = is_newer_version(ARCHON_VERSION, latest_version) + + # Parse published date + published_at = None + if release.get("published_at"): + try: + published_at = datetime.fromisoformat( + release["published_at"].replace("Z", "+00:00") + ) + except Exception: + pass + + return { + "current": ARCHON_VERSION, + "latest": latest_version, + "update_available": update_available, + "release_url": release.get("html_url"), + "release_notes": release.get("body"), + "published_at": published_at, + "check_error": None, + "assets": release.get("assets", []), + "author": release.get("author", {}).get("login"), + } + + except Exception as e: + logfire.error(f"Error checking for updates: {e}") + # Return safe default with error + return { + "current": ARCHON_VERSION, + "latest": None, + "update_available": False, + "release_url": None, + "release_notes": None, + "published_at": None, + "check_error": str(e), + } + + def clear_cache(self): + """Clear the cached version data.""" + self._cache = None + self._cache_time = None + + +# Export singleton instance +version_service = VersionService() diff --git a/python/src/server/utils/semantic_version.py b/python/src/server/utils/semantic_version.py new file mode 100644 index 00000000..d869f7a8 --- /dev/null +++ b/python/src/server/utils/semantic_version.py @@ -0,0 +1,107 @@ +""" +Semantic version parsing and comparison utilities. +""" + +import re + + +def parse_version(version_string: str) -> tuple[int, int, int, str | None]: + """ + Parse a semantic version string into major, minor, patch, and optional prerelease. + + Supports formats like: + - "1.0.0" + - "v1.0.0" + - "1.0.0-beta" + - "v1.0.0-rc.1" + + Args: + version_string: Version string to parse + + Returns: + Tuple of (major, minor, patch, prerelease) + """ + # Remove 'v' prefix if present + version = version_string.strip() + if version.lower().startswith('v'): + version = version[1:] + + # Parse version with optional prerelease + pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$' + match = re.match(pattern, version) + + if not match: + # Try to handle incomplete versions like "1.0" + simple_pattern = r'^(\d+)(?:\.(\d+))?(?:\.(\d+))?$' + simple_match = re.match(simple_pattern, version) + if simple_match: + major = int(simple_match.group(1)) + minor = int(simple_match.group(2) or 0) + patch = int(simple_match.group(3) or 0) + return (major, minor, patch, None) + raise ValueError(f"Invalid version string: {version_string}") + + major = int(match.group(1)) + minor = int(match.group(2)) + patch = int(match.group(3)) + prerelease = match.group(4) + + return (major, minor, patch, prerelease) + + +def compare_versions(version1: str, version2: str) -> int: + """ + Compare two semantic version strings. + + Args: + version1: First version string + version2: Second version string + + Returns: + -1 if version1 < version2 + 0 if version1 == version2 + 1 if version1 > version2 + """ + v1 = parse_version(version1) + v2 = parse_version(version2) + + # Compare major, minor, patch + for i in range(3): + if v1[i] < v2[i]: + return -1 + elif v1[i] > v2[i]: + return 1 + + # If main versions are equal, check prerelease + # No prerelease is considered newer than any prerelease + if v1[3] is None and v2[3] is None: + return 0 + elif v1[3] is None: + return 1 # v1 is release, v2 is prerelease + elif v2[3] is None: + return -1 # v1 is prerelease, v2 is release + else: + # Both have prereleases, compare lexicographically + if v1[3] < v2[3]: + return -1 + elif v1[3] > v2[3]: + return 1 + return 0 + + +def is_newer_version(current: str, latest: str) -> bool: + """ + Check if latest version is newer than current version. + + Args: + current: Current version string + latest: Latest version string to compare + + Returns: + True if latest > current, False otherwise + """ + try: + return compare_versions(latest, current) > 0 + except ValueError: + # If we can't parse versions, assume no update + return False