ProjectsParts Services
Admin Environment Switcher - Feature Plan
Admin panel feature for switching data environments and managing K&M Tire integration.
Admin Environment Switcher - Feature Plan
Overview
Admin panel feature for switching data environments and managing K&M Tire integration.
Goals
- Environment Switching: Switch between
crop_dev,crop_stage,crop_proddatabases - K&M Tire Toggle: Enable/disable K&M Tire data in the search index
- Index Rebuild: Trigger Elasticsearch index rebuild from admin UI
- Status Monitoring: View current configuration and rebuild progress
Architecture
Current State
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CROP-Front │────▶│ Search Service │────▶│ Elasticsearch │
│ (Next.js) │ │ (Hono) │ │ Index │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ MongoDB │
│ (crop_dev) │
└─────────────────┘Target State
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CROP-Front │────▶│ Search Service │────▶│ Elasticsearch │
│ Admin Panel │ │ Admin API │ │ Index │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ MongoDB │
│ ├─────────────────┤
│ │ • crop_dev │
│ │ • crop_stage │
│ │ • crop_prod │
│ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
└──────────────▶│ Catalog Service │
│ (K&M Tire) │
└─────────────────┘API Design
Search Service Admin Endpoints
1. Get Current Configuration
GET /api/admin/index-config
Response: {
environment: "crop_dev" | "crop_stage" | "crop_prod",
indexName: "parts_current",
aliasTarget: "parts_v2026-01-16t0741",
documentCount: 1639,
manufacturers: [
{ code: "BNS", name: "Briggs & Stratton", count: 1313 },
{ code: "VNT", name: "Ventrac", count: 211 },
{ code: "KMT", name: "K&M Tire", count: 109, enabled: true },
{ code: "NHL", name: "New Holland", count: 5 }
],
lastRebuild: "2026-01-16T07:41:00Z",
collections: ["parts_nhl", "parts_bns", "parts_vnt", "parts_kmt"]
}2. Trigger Index Rebuild
POST /api/admin/rebuild-index
Body: {
environment: "crop_dev" | "crop_stage" | "crop_prod",
includeKmTire: true,
collections?: string[] // Optional: specific collections to include
}
Response: {
jobId: "rebuild-2026-01-16-abc123",
status: "started",
estimatedDuration: "2-3 minutes"
}3. Get Rebuild Status
GET /api/admin/rebuild-status/{jobId}
Response: {
jobId: "rebuild-2026-01-16-abc123",
status: "in_progress" | "completed" | "failed",
progress: {
phase: "indexing",
current: 1200,
total: 1639,
percentage: 73
},
startedAt: "2026-01-16T07:41:00Z",
completedAt: null,
error: null
}4. List Available Environments
GET /api/admin/environments
Response: {
environments: [
{
id: "crop_dev",
name: "Development",
collections: ["parts_nhl", "parts_bns", "parts_vnt", "parts_kmt"],
totalDocuments: 1639,
lastSync: "2026-01-16T06:57:00Z"
},
{
id: "crop_stage",
name: "Staging (Small Dataset)",
collections: ["parts_nhl", "parts_bns", "parts_vnt", "parts_kmt"],
totalDocuments: 118,
lastSync: "2026-01-15T12:00:00Z"
},
{
id: "crop_prod",
name: "Production",
collections: ["parts_nhl", "parts_bns", "parts_vnt"],
totalDocuments: 12500,
lastSync: "2026-01-16T00:00:00Z"
}
],
current: "crop_dev"
}Backend Implementation
File Structure
services/search/src/
├── routes/
│ └── admin.ts # Existing admin routes
│ └── admin-index.ts # NEW: Index management routes
├── services/
│ └── index-rebuild.ts # NEW: Index rebuild service
│ └── env-manager.ts # NEW: Environment management
├── jobs/
│ └── rebuild-job.ts # NEW: Background rebuild job
└── schemas/
└── admin-index.ts # NEW: Request/response schemasKey Components
1. Environment Manager (env-manager.ts)
const ENVIRONMENTS = {
crop_dev: {
uri: process.env.MONGODB_URI_DEV,
collections: ['parts_nhl', 'parts_bns', 'parts_vnt', 'parts_kmt']
},
crop_stage: {
uri: process.env.MONGODB_URI_STAGE,
collections: ['parts_nhl', 'parts_bns', 'parts_vnt', 'parts_kmt']
},
crop_prod: {
uri: process.env.MONGODB_URI_PROD,
collections: ['parts_nhl', 'parts_bns', 'parts_vnt']
}
} as const;
export async function getEnvironmentStats(envId: string): Promise<EnvStats> {
// Connect to specified MongoDB, count documents per collection
}
export async function switchEnvironment(envId: string): Promise<void> {
// Update current environment in memory/config
}2. Index Rebuild Service (index-rebuild.ts)
interface RebuildOptions {
environment: 'crop_dev' | 'crop_stage' | 'crop_prod';
includeKmTire: boolean;
collections?: string[];
}
interface RebuildJob {
id: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
progress: { current: number; total: number; phase: string };
startedAt: Date;
completedAt?: Date;
error?: string;
}
// In-memory job tracking (or use Redis for multi-instance)
const activeJobs = new Map<string, RebuildJob>();
export async function startRebuild(options: RebuildOptions): Promise<string> {
const jobId = `rebuild-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
// Create job entry
activeJobs.set(jobId, {
id: jobId,
status: 'pending',
progress: { current: 0, total: 0, phase: 'initializing' },
startedAt: new Date()
});
// Start background rebuild (non-blocking)
runRebuildInBackground(jobId, options);
return jobId;
}
export function getJobStatus(jobId: string): RebuildJob | null {
return activeJobs.get(jobId) || null;
}3. Background Rebuild Logic
async function runRebuildInBackground(jobId: string, options: RebuildOptions) {
const job = activeJobs.get(jobId)!;
try {
job.status = 'in_progress';
job.progress.phase = 'connecting';
// 1. Connect to source MongoDB
const db = await connectToEnvironment(options.environment);
// 2. Count total documents
const collections = options.collections ||
ENVIRONMENTS[options.environment].collections;
// Filter out K&M Tire if disabled
const filteredCollections = options.includeKmTire
? collections
: collections.filter(c => c !== 'parts_kmt');
let totalDocs = 0;
for (const coll of filteredCollections) {
totalDocs += await db.collection(coll).countDocuments();
}
job.progress.total = totalDocs;
// 3. Create new versioned index
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
const newIndexName = `parts_v${timestamp}`;
job.progress.phase = 'creating_index';
await createIndex(newIndexName);
// 4. Index documents with progress updates
job.progress.phase = 'indexing';
let indexed = 0;
for (const collName of filteredCollections) {
const cursor = db.collection(collName).find();
const batch = [];
for await (const doc of cursor) {
const transformed = passthroughTransform(doc);
if (transformed) {
batch.push(transformed);
if (batch.length >= 500) {
await bulkIndex(newIndexName, batch);
indexed += batch.length;
job.progress.current = indexed;
batch.length = 0;
}
}
}
// Index remaining
if (batch.length > 0) {
await bulkIndex(newIndexName, batch);
indexed += batch.length;
job.progress.current = indexed;
}
}
// 5. Switch alias
job.progress.phase = 'switching_alias';
await switchAlias(newIndexName);
// 6. Complete
job.status = 'completed';
job.completedAt = new Date();
job.progress.phase = 'completed';
} catch (error) {
job.status = 'failed';
job.error = error.message;
job.completedAt = new Date();
}
}Frontend Implementation
File Structure
app/
├── admin/
│ └── index/
│ └── page.tsx # Admin index management page
│ └── _components/
│ ├── environment-selector.tsx
│ ├── km-tire-toggle.tsx
│ ├── rebuild-button.tsx
│ ├── rebuild-progress.tsx
│ └── index-stats.tsx
└── api/
└── admin/
└── [...path]/
└── route.ts # Proxy to search service admin APIUI Components
1. Admin Index Page (/admin/index)
export default function AdminIndexPage() {
return (
<AdminLayout>
<h1>Index Management</h1>
<div className="grid gap-6">
{/* Current Status Card */}
<IndexStatsCard />
{/* Environment Selector */}
<EnvironmentSelector />
{/* K&M Tire Toggle */}
<KmTireToggle />
{/* Rebuild Controls */}
<RebuildControls />
{/* Rebuild Progress (when active) */}
<RebuildProgress />
</div>
</AdminLayout>
);
}2. Environment Selector
function EnvironmentSelector() {
const [environments, setEnvironments] = useState<Environment[]>([]);
const [current, setCurrent] = useState<string>('');
return (
<Card>
<CardHeader>
<CardTitle>Data Environment</CardTitle>
<CardDescription>
Select which MongoDB database to use for the search index
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={current} onValueChange={setCurrent}>
{environments.map(env => (
<div key={env.id} className="flex items-center space-x-4 p-4 border rounded">
<RadioGroupItem value={env.id} />
<div className="flex-1">
<Label>{env.name}</Label>
<p className="text-sm text-muted-foreground">
{env.totalDocuments.toLocaleString()} documents
</p>
</div>
<Badge variant={env.id === current ? 'default' : 'outline'}>
{env.id === current ? 'Active' : 'Available'}
</Badge>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
);
}3. K&M Tire Toggle
function KmTireToggle() {
const [enabled, setEnabled] = useState(true);
return (
<Card>
<CardHeader>
<CardTitle>K&M Tire Integration</CardTitle>
<CardDescription>
Include K&M Tire products in the search index
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-neutral-100 flex items-center justify-center">
T
</div>
<div>
<p className="font-medium">K&M Tire (109 products)</p>
<p className="text-sm text-muted-foreground">
Wholesale tire catalog with brand logos
</p>
</div>
</div>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</div>
</CardContent>
</Card>
);
}4. Rebuild Progress
function RebuildProgress({ jobId }: { jobId: string }) {
const { data: job, isLoading } = useQuery({
queryKey: ['rebuild-status', jobId],
queryFn: () => fetchRebuildStatus(jobId),
refetchInterval: job?.status === 'in_progress' ? 1000 : false
});
if (!job) return null;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{job.status === 'in_progress' && <Loader2 className="animate-spin" />}
{job.status === 'completed' && <CheckCircle className="text-green-500" />}
{job.status === 'failed' && <XCircle className="text-red-500" />}
Index Rebuild
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span>{job.progress.phase}</span>
<span>{job.progress.percentage}%</span>
</div>
<Progress value={job.progress.percentage} />
</div>
<p className="text-sm text-muted-foreground">
{job.progress.current.toLocaleString()} / {job.progress.total.toLocaleString()} documents
</p>
</div>
</CardContent>
</Card>
);
}Environment Variables
Search Service
# Multiple MongoDB connection strings
MONGODB_URI_DEV=mongodb+srv://...@crop-gcp.mongodb.net/crop_dev
MONGODB_URI_STAGE=mongodb+srv://...@crop-gcp.mongodb.net/crop_stage
MONGODB_URI_PROD=mongodb+srv://...@crop-gcp.mongodb.net/crop_prod
# Current active environment (set via admin or env var)
ACTIVE_ENVIRONMENT=crop_dev
# Admin authentication
ADMIN_API_TOKEN=your-secure-tokenSecurity Considerations
- Authentication: All admin endpoints require
X-Admin-Tokenheader - Authorization: Only users with admin role can access admin pages
- Rate Limiting: Limit rebuild requests to 1 per minute
- Audit Logging: Log all admin actions with user ID and timestamp
- Confirmation: Require confirmation for production environment changes
Implementation Phases
Phase 1: Backend API (2-3 hours)
- Create
admin-index.tsroutes - Implement
env-manager.tsservice - Implement
index-rebuild.tsservice - Add background job tracking
Phase 2: Frontend UI (2-3 hours)
- Create
/admin/indexpage - Implement environment selector component
- Implement K&M Tire toggle
- Implement rebuild button with progress
Phase 3: Integration & Testing (1-2 hours)
- Add API proxy route in Next.js
- Test environment switching
- Test rebuild functionality
- Add error handling and edge cases
Future Enhancements
- Scheduled Rebuilds: Set up automatic nightly index rebuilds
- Diff View: Show changes before applying environment switch
- Rollback: One-click rollback to previous index version
- Webhook Notifications: Notify Slack/Discord on rebuild completion
- Multi-region: Support for different ES clusters per environment