chore: sync with Gitea master and restore local-only files

Reset local main to gitea/master (new source of truth) and restored
local-only files: web scrapers, admin dashboard, ChromaDB integration,
debug scripts, and utility libraries that aren't tracked in Gitea.

Gitea master adds: discovermass, buscarmisas-network, hk-parishes,
bohosluzby, kerknet, gottesdienstzeiten, miserend importers,
ClaimRequest model, forward geocoding, heartbeat healthcheck.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Albert
2026-04-12 19:11:22 -04:00
parent 76cca3ba75
commit 2c51513851
133 changed files with 30381 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env tsx
/**
* Export churches from local NAS database and import to Neon
*/
import { PrismaClient } from '@prisma/client';
import fs from 'fs/promises';
import path from 'path';
interface ExportStats {
churches: number;
massSchedules: number;
confessionSchedules: number;
adorationSchedules: number;
}
async function exportFromNAS(country: string): Promise<ExportStats> {
console.log(`📦 Exporting ${country} data from NAS...`);
// Set DATABASE_URL to NAS
const originalUrl = process.env.DATABASE_URL;
process.env.DATABASE_URL = 'postgresql://postgres:postgres@192.168.0.145:5432/nearestmass';
const nasPrisma = new PrismaClient();
try {
await nasPrisma.$connect();
console.log('✅ Connected to NAS database');
// Export churches with all schedules
const churches = await nasPrisma.churches.findMany({
where: { country },
include: {
massSchedules: true,
confessionSchedules: true,
adorationSchedules: true,
}
});
console.log(`Found ${churches.length} churches in ${country}`);
// Count schedules
const stats: ExportStats = {
churches: churches.length,
massSchedules: churches.reduce((sum, c) => sum + (c.massSchedules?.length || 0), 0),
confessionSchedules: churches.reduce((sum, c) => sum + (c.confessionSchedules?.length || 0), 0),
adorationSchedules: churches.reduce((sum, c) => sum + (c.adorationSchedules?.length || 0), 0),
};
// Save to file
const exportFile = path.join(process.cwd(), `export-${country}.json`);
await fs.writeFile(exportFile, JSON.stringify(churches, null, 2));
console.log(`✅ Exported to ${exportFile}`);
console.log(` - ${stats.churches} churches`);
console.log(` - ${stats.massSchedules} mass schedules`);
console.log(` - ${stats.confessionSchedules} confession schedules`);
console.log(` - ${stats.adorationSchedules} adoration schedules`);
await nasPrisma.$disconnect();
// Restore original DATABASE_URL
if (originalUrl) {
process.env.DATABASE_URL = originalUrl;
}
return stats;
} catch (error) {
console.error('❌ Export failed:', error);
await nasPrisma.$disconnect();
// Restore original DATABASE_URL
if (originalUrl) {
process.env.DATABASE_URL = originalUrl;
}
throw error;
}
}
async function importToNeon(country: string, dryRun: boolean = true): Promise<void> {
console.log(`\n📤 Importing ${country} data to Neon...`);
if (dryRun) {
console.log('🔍 DRY RUN MODE - No data will be written');
}
// Read export file
const exportFile = path.join(process.cwd(), `export-${country}.json`);
const data = JSON.parse(await fs.readFile(exportFile, 'utf-8'));
console.log(`Loaded ${data.length} churches from export file`);
// Connect to Neon
const neonPrisma = new PrismaClient();
try {
await neonPrisma.$connect();
console.log('✅ Connected to Neon database');
let inserted = 0;
let updated = 0;
let errors = 0;
for (const church of data) {
try {
const massSchedules = church.massSchedules || [];
const confessionSchedules = church.confessionSchedules || [];
const adorationSchedules = church.adorationSchedules || [];
// Remove relations and ids
delete church.massSchedules;
delete church.confessionSchedules;
delete church.adorationSchedules;
delete church.id;
if (!dryRun) {
// Upsert church based on coordinates
const result = await neonPrisma.churches.upsert({
where: {
latitude_longitude: {
latitude: church.latitude,
longitude: church.longitude
}
},
create: church,
update: church
});
// Check if it was an insert or update
const existing = await neonPrisma.churches.findFirst({
where: {
latitude: church.latitude,
longitude: church.longitude,
createdAt: { lt: new Date(Date.now() - 1000) } // Created more than 1 sec ago
}
});
if (existing) {
updated++;
} else {
inserted++;
}
// Insert schedules
for (const schedule of massSchedules) {
delete schedule.id;
await neonPrisma.massSchedules.create({
data: {
...schedule,
churchId: result.id
}
});
}
for (const schedule of confessionSchedules) {
delete schedule.id;
await neonPrisma.confessionSchedules.create({
data: {
...schedule,
churchId: result.id
}
});
}
for (const schedule of adorationSchedules) {
delete schedule.id;
await neonPrisma.adorationSchedules.create({
data: {
...schedule,
churchId: result.id
}
});
}
} else {
// Dry run - just count
inserted++;
}
if (inserted % 100 === 0) {
console.log(`Progress: ${inserted + updated} churches processed...`);
}
} catch (error) {
errors++;
console.error(`Error importing church ${church.name}:`, error instanceof Error ? error.message : error);
}
}
console.log('\n✅ Import complete!');
console.log(` - ${inserted} churches inserted`);
console.log(` - ${updated} churches updated`);
console.log(` - ${errors} errors`);
await neonPrisma.$disconnect();
} catch (error) {
console.error('❌ Import failed:', error);
await neonPrisma.$disconnect();
throw error;
}
}
async function main() {
const country = process.argv[2] || 'PL';
const mode = process.argv[3] || 'dry-run';
const dryRun = mode === 'dry-run';
console.log('🌍 Export/Import to Neon');
console.log('========================\n');
try {
// Step 1: Export from NAS
const stats = await exportFromNAS(country);
// Step 2: Import to Neon
await importToNeon(country, dryRun);
if (dryRun) {
console.log('\n💡 This was a DRY RUN. To actually import to Neon, run:');
console.log(` npx tsx scripts/export-import-to-neon.ts ${country} real-import`);
} else {
console.log('\n🎉 Data successfully uploaded to Neon!');
}
} catch (error) {
console.error('❌ Process failed:', error);
process.exit(1);
}
}
main().catch(console.error);