1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as api } from "./index-D0Bmh-UX.js"; 3import ArchiveImporter from "./ArchiveImporter-n0Q0QYy4.js"; 4import { d_ as ArrowLeftRight, cY as ChevronLeft, cL as RefreshCw, aY as TriangleAlert, a_ as CircleCheck, b3 as FileHeadphone, bU as Play, bH as Hash, cA as ChevronUp, cm as ArrowRight, b5 as Trash2, dX as Server, b2 as Upload, ch as Database, b9 as Settings, fG as Map$1, cG as Tag, bE as ChevronRight, df as SlidersVertical, cB as ChevronDown } from "./vendor-dVNaLJx_.js"; 5import "./vendor-i18n-R-bMoYp_.js"; 6import "./synth-BE1TSTD4.js"; 7import "./vendor-react-three-MDLA5jog.js"; 8import "./wasm-synth-adapter-BrqgBX1y.js"; 9import "./vendor-spessasynth-DH8n6o_V.js"; 10import "./vendor-audio-DH3w0BzW.js"; 11const PRESETS = [ 12 { id: "smart_migration", title: "â¡ Smart Migration", description: "One-button catch-up migration for all database tables and files in correct order.", table: "", targetModel: "", database: "" }, 13 { id: "custom", title: "Custom Migration", description: "Manually configure source table and target model.", table: "", targetModel: "", database: "" }, 14 { id: "users", title: "Preset: Users", description: "Migrate users from `hamienet_OEML.users`. Handles password hash conversion.", table: "users", targetModel: "User", database: "hamienet_O
14EML" }, 15 { id: "tracks", title: "Preset: Tracks", description: "Migrate `hamienet_MIDI.Links` table. Maps LinkOwner to User, Note to Artist/Tag.", table: "Links", targetModel: "Midi", database: "hamienet_MIDI" }, 16 { id: "comments", title: "Preset: Comments", description: "Migrate `hamienet_MIDI.Reviews` table to Comments. Auto-calculates score.", table: "Reviews", targetModel: "Comment", database: "hamienet_MIDI" }, 17 { id: "tags_legacy", title: "Preset: Tags (Explicit)", description: "Migrate `hamienet_MIDI.TagsLink` joined with `Tags`. Connects explicit tags to tracks.", table: "TagsLink", targetModel: "Tag", database: "hamienet_MIDI" }, 18 { id: "midi_blobs", title: "Tool: MIDI Blob Extractor", description: "Extract binary MIDI data from legacy `MIDI` table directly to disk.", table: "MIDI", targetModel: "File System", database: "hamienet_MIDI" }, 19 { id: "avatar_migration", title: "Tool: Avatar Migration", description: "Migrate legacy avatars from flat folder to structured storage and resolve DB links.", table: "User", targetModel: "Storage", database: "hamienet_O
19EML" }, 20 { id: "generate_handles", title: "Tool: Generate Handles", description: "One-time tool to generate unique @handles for all legacy users.", table: "User", targetModel: "User", database: "Prisma" }, 21 { id: "archive_importer", title: "Tool: MIDI Archive Importer", description: "Upload a local folder of MIDI files, parse path-based artists/titles, check hashes, and import in resilient chunks.", table: "Local File System", targetModel: "Midi", database: "Local Folder" } 22]; 23const USER_PRESET_MAP = { 24 // Screenshot 1 25 id: "user_id", 26 email: "user_email", 27 name: "username", 28 // Mapped to DB column 'name' (Prisma @map("name")) 29 passwordHash: "", 30 // -- Ignore -- 31 legacyPasswordHash: "user_password", 32 avatar: "user_avatar", 33 banner: "", 34 // -- Ignore -- 35 description: "", 36 // -- Ignore -- 37 role: "", 38 // -- Ignore -- (will default to USER/ADMIN based on logic if needed) 39 isBanned: "", 40 // -- Ignore -- 41 trustScore: "", 42 // Not in legacy 43 // Screenshot 2 44 createdAt: "user_regdate", 45 updatedAt: "user_lastvisit", 46 canMessage: "", 47 // -- Ignore -- 48 credits: "", 49 // -- Ignore -- 50 lastCreditReset: "", 51 // -- Ignore -- 52 subscriptionTier: "", 53 // -- Ignore -- 54 emailVerified: "", 55 // -- Ignore -- 56 emailVerificationToken: "", 57 // -- Ignore -- 58 website: "user_website", 59 location: "user_from", 60 signature: "user_sig", 61 // Screenshot 3 62 occupation: "user_occ", 63 interests: "user_interests", 64 icq: "user_icq", 65 aim: "user_aim", 66 yim: "user_yim", 67 msnm: "user_msnm", 68 legacyMidiCount: "user_midi", 69 legacyPostCount: "user_posts", 70 timezone: "user_timezone", 71 dateFormat: "user_dateformat", 72 lastVisit: "user_lastvisit", 73 // Screenshot 4 74 donateAmount: "user_donate_amount", 75 donateExpire: "user_donate_expire", 76 language_id: "" 77 // -- Ignore -- 78}; 79const TRACK_PRESET_MAP = { 80 id: "ID", 81 title: "Title", 82 artist: "Note", 83 // Map Note to Artist as requested 84 description: "Description", 85 userId: "LinkOwner", 86 // Will be resolved to ID on server 87 thumbnail: "poller", 88 // Legacy heuristic 89 views: "Views", 90 plays: "Hits", 91 likesCount: "Rating", 92 // Legacy heuristic 93 midiPath: "url", 94 // The actual file link 95 // Dates 96 createdAt: "Add_Date", 97 updatedAt: "Mod_Date", 98 uploadedAt: "Add_Date", 99 // Same as created 100 // Ignore Status (force default) 101 status: "", 102 // Ignore Relations 103 instruments: "", 104 midiMetadata: "" 105}; 106const COMMENT_PRESET_MAP = { 107 id: "ReviewID", 108 midiId: "Review_LinkID", 109 userId: "Review_Owner", 110 // Will be resolved to ID on server 111 text: "Review_Contents", 112 // Server will prepend Subject 113 createdAt: "Review_Date", 114 likes: "Review_WasHelpful", 115 score: "", 116 // Server will calculate (Helpful - NotHelpful) 117 status: "Review_Validated" 118}; 119const DataMigration = () => { 120 var _a; 121 const [activePreset, setActivePreset] = reactExports.useState("smart_migration"); 122 const [smartMigrationStatus, setSmartMigrationStatus] = reactExports.useState("idle"); 123 const [smartMigrationResult, setSmartMigrationResult] = reactExports.useState([]); 124 const [isSidebarOpen, setIsSidebarOpen] = reactExports.useState(true); 125 const [config, setConfig] = reactExports.useState({ sourceTable: "", targetModel: "" }); 126 const [sourceMode, setSourceMode] = reactExports.useState("direct"); 127 const [dbConfig, setDbConfig] = reactExports.useState({ 128 host: "192.168.1.134", 129 user: "root", 130 password: "", 131 database: "hamienet_MIDI", 132 port: 3306 133 }); 134 const [fetchLimit, setFetchLimit] = reactExports.useState(0); 135 const [fetchStartId, setFetchStartId] = reactExports.useState(0); 136 const PREVIEW_LIMIT = 50; 137 const [blobStartId, setBlobStartId] = reactExports.useState(0); 138 const [blobLimit, setBlobLimit] = reactExports.useState(50); 139 const [maxExtractedId, setMaxExtractedId] = reactExports.useState(0); 140 const [continueFromLatest, setContinueFromLatest] = reactExports.useState(true); 141 const [remainingCount, setRemainingCount] = reactExports.useState(0); 142 const [isHeaderOpen, setIsHeaderOpen] = reactExports.useState(true); 143 const [isLogsOpen, setIsLogsOpen] = reactExports.useState(true); 144 const [dbSchema, setDbSchema] = reactExports.useState([]); 145 const [availableModels, setAvailableModels] = reactExports.useState([]); 146 const [targetFields, setTargetFields] = reactExports.useState([]); 147 const [fieldMapping, setFieldMapping] = reactExports.useState({}); 148 const [sourceFields, setSourceFields] = reactExports.useState([]); 149 const [sourceData, setSourceData] = reactExports.useState([]); 150 const [previewIndex, setPreviewIndex] = reactExports.useState(0); 151 const [previewData, setPreviewData] = reactExports.useState([]); 152 const [isProcessing, setIsProcessing] = reactExports.useState(false); 153 const [isLoadingSchema, setIsLoadingSchema] = reactExports.useState(false); 154 const [logs, setLogs] = reactExports.useState([]); 155 const [successCount, setSuccessCount] = reactExports.useState(0); 156 const [overwrite, setOverwrite] = reactExports.useState(false); 157 const [verbose, setVerbose] = reactExports.useState(false); 158 const [verboseErrorOnly, setVerboseErrorOnly] = reactExports.useState(false); 159 const [resetBeforeMigrating, setResetBeforeMigrating] = reactExports.useState(false); 160 const [catchUpData, setCatchUpData] = reactExports.useState(null); 161 const [isCountingCatchUp, setIsCountingCatchUp] = reactExports.useState(false); 162 reactExports.useEffect(() => { 163 setCatchUpData(null); 164 }, [fetchLimit, fetchStartId, config, dbConfig, fieldMapping, overwrite]); 165 reactExports.useEffect(() => { 166 const loadSchema = async () => { 167 setIsLoadingSchema(true);
168 try { 169 const schema = await api.getDatabaseSchema(); 170 setDbSchema(schema); 171 const models = schema.map((t) => t.table); 172 setAvailableModels(models); 173 } catch (e) { 174 console.error("Failed to load schema", e); 175 } finally { 176 setIsLoadingSchema(false); 177 } 178 }; 179 loadSchema(); 180 }, []); 181 const fetchMidiExtractionStatus = async () => { 182 try { 183 const res = await api.getMidiExtractionStatus(dbConfig); 184 setMaxExtractedId(res.maxExtractedId); 185 setRemainingCount(res.remainingCount); 186 } catch (e) { 187 console.error("Failed to fetch MIDI extraction status", e); 188 } 189 }; 190 reactExports.useEffect(() => { 191 fetchMidiExtractionStatus(); 192 }, []); 193 reactExports.useEffect(() => { 194 if (activePreset === "midi_blobs") { 195 fetchMidiExtractionStatus(); 196 } 197 }, [activePreset, dbConfig]); 198 reactExports.useEffect(() => { 199 if (continueFromLatest) { 200 setBlobStartId(maxExtractedId + 1); 201 } 202 }, [maxExtractedId, continueFromLatest]); 203 reactExports.useEffect(() => { 204 const fetchTargetFields = async () => { 205 if (!config.targetModel) { 206 setTargetFields([]); 207 return; 208 } 209 try { 210 const tableDef = dbSchema.find((t) => t.table === config.targetModel); 211 if (tableDef) { 212 const fields = tableDef.columns.map((c) => c.name); 213 setTargetFields(fields); 214 setFieldMapping({}); 215 } 216 } catch (e) { 217 console.error(e); 218 } 219 }; 220 fetchTargetFields(); 221 }, [config.targetModel, dbSchema]); 222 reactExports.useEffect(() => { 223 const preset = PRESETS.find((p) => p.id === activePreset); 224 if (preset && preset.id !== "custom") { 225 setConfig({ 226 sourceTable: preset.table, 227 targetModel: preset.targetModel 228 }); 229 if (preset.database) { 230 setDbConfig((prev) => ({ ...prev, database: preset.database })); 231 } 232 setSourceData([]); 233 setPreviewIndex(0); 234 setIsSidebarOpen(false); 235 } else if (preset && preset.id === "custom") { 236 setConfig({ sourceTable: "", targetModel: "" }); 237 setSourceData([]); 238 } 239 }, [activePreset]); 240 reactExports.useEffect(() => { 241 if (sourceData.length > 0) { 242 transformData(sourceData); 243 } 244 }, [sourceData, fieldMapping, config.targetModel]); 245 const handleFileUpload = (e) => { 246 var _a2; 247 const file = (_a2 = e.target.files) == null ? void 0 : _a2[0]; 248 if (!file) return; 249 const reader = new FileReader(); 250 reader.onload = (ev) => { 251 var _a3; 252 try { 253 const text = (_a3 = ev.target) == null ? void 0 : _a3.result; 254 let data = []; 255 if (file.name.endsWith(".json")) { 256 data = JSON.parse(text); 257 } else { 258 alert("SQL parsing not implemented in browser. Please use JSON export script provided."); 259 return; 260 } 261 if (Array.isArray(data)) { 262 setSourceData(data); 263 setPreviewIndex(0); 264 if (data.length > 0) { 265 const keys = Object.keys(data[0]); 266 setSourceFields(keys); 267 autoMapFields(keys, targetFields); 268 } 269 } 270 } catch (err) { 271 alert("Failed to parse file: " + err.message); 272 } 273 }; 274 reader.readAsText(file); 275 }; 276 const handleDirectFetch = async () => { 277 if (!config.sourceTable) { 278 alert("Please specify a Source Table."); 279 return; 280 } 281 setIsProcessing(true); 282 setLogs((prev) => [...prev, `Connecting to Legacy DB at ${dbConfig.host}... (Fetching preview of ${PREVIEW_LIMIT} records)`]); 283 try { 284 const data = await api.fetchSourceData(dbConfig, config.sourceTable, PREVIEW_LIMIT, config.targetModel); 285 setSourceData(data); 286 setPreviewIndex(0); 287 if (data.length > 0) { 288 const keys = Object.keys(data[0]); 289 setSourceFields(keys); 290 autoMapFields(keys, targetFields); 291 setLogs((prev) => [...prev, `Fetched ${data.length} preview records.`]); 292 setIsHeaderOpen(false); 293 } else { 294 setLogs((prev) => [...prev, `Fetched 0 records.`]); 295 } 296 } catch (e) { 297 console.error("Migration Error:", e); 298 setLogs((prev) => [...prev, `ERROR: ${e.message}`]); 299 alert(`Failed to fetch data: ${e.message}`); 300 } finally { 301 setIsProcessing(false); 302 } 303 }; 304 const handleSmartMigration = async () => { 305 setIsProcessing(true); 306 setSmartMigrationStatus("running"); 307 setSmartMigrationResult([]);
308 setLogs((prev) => [...prev, `â¡ Starting 1-Button Smart Migration sequence...`]); 309 const runStep = async (stepName, fn) => { 310 const startTime = Date.now(); 311 setLogs((prev) => [...prev, `[Smart Migration] Running step: ${stepName}...`]); 312 try { 313 const res = await fn(); 314 const durationMs = Date.now() - startTime; 315 setLogs((prev) => [...prev, `â [Smart Migration] ${stepName} completed successfully in ${durationMs}ms.`]); 316 const processed = res.processed ?? (res.migratedCount !== void 0 ? res.migratedCount + (res.defaultedCount ?? 0) : res.count ?? 0); 317 const inserted = res.inserted ?? (res.migratedCount !== void 0 ? res.migratedCount : res.processed !== void 0 ? res.processed : res.count ?? 0); 318 const skipped = res.skipped ?? res.conflicts ?? res.defaultedCount ?? 0; 319 const errors = res.errors ?? 0; 320 return { 321 step: stepName, 322 status: "completed", 323 processed, 324 inserted, 325 skipped, 326 errors, 327 durationMs 328 }; 329 } catch (err) { 330 const durationMs = Date.now() - startTime; 331 setLogs((prev) => [...prev, `â [Smart Migration] ${stepName} failed: ${err.message}`]); 332 return { 333 step: stepName, 334 status: "failed", 335 processed: 0, 336 inserted: 0, 337 skipped: 0, 338 errors: 1, 339 details: err.message, 340 durationMs 341 }; 342 } 343 }; 344 const stepsResults = []; 345 const usersRes = await runStep( 346 "Users", 347 () =>
347 api.executeMigration({ ...dbConfig, database: "hamienet_OEML" }, "users", "User", USER_PRESET_MAP, 0, 0, false, false) 348 ); 349 stepsResults.push(usersRes); 350 setSmartMigrationResult([...stepsResults]); 351 if (usersRes.status === "failed") { 352 setSmartMigrationStatus("error"); 353 setIsProcessing(false); 354 return; 355 } 356 const tracksRes = await runStep( 357 "Tracks", 358 () => api.executeMigration({ ...dbConfig, database: "hamienet_MIDI" }, "Links", "Midi", TRACK_PRESET_MAP, 0, 0, false, false) 359 ); 360 stepsResults.push(tracksRes); 361 setSmartMigrationResult([...stepsResults]); 362 if (tracksRes.status === "failed") { 363 setSmartMigrationStatus("error"); 364 setIsProcessing(false); 365 return; 366 } 367 const commentsRes = await runStep( 368 "Comments", 369 () => api.executeMigration({ ...dbConfig, database: "hamienet_MIDI" }, "Reviews", "Comment", COMMENT_PRESET_MAP, 0, 0, false, false) 370 ); 371 stepsResults.push(commentsRes); 372 setSmartMigrationResult([...stepsResults]); 373 if (commentsRes.status === "failed") { 374 setSmartMigrationStatus("error"); 375 setIsProcessing(false); 376 return; 377 } 378 const tagsRes = await runStep( 379 "Tags", 380 () => api.executeMigration({ ...dbConfig, database: "hamienet_MIDI" }, "TagsLink", "Tag", {}, 0, 0, false, false) 381 ); 382 stepsResults.push(tagsRes); 383 setSmartMigrationResult([...stepsResults]); 384 if (tagsRes.status === "failed") { 385 setSmartMigrationStatus("error"); 386 setIsProcessing(false); 387 return; 388 } 389 const blobsRes = await runStep("MIDI Blobs", async () => { 390 const status = await api.getMidiExtractionStatus({ ...dbConfig, database: "hamienet_MIDI" }); 391 const extractRes = await api.extractMidiFiles({ ...dbConfig, database: "hamienet_MIDI" }, status.maxExtractedId + 1, 0); 392 return { 393 count: extractRes.count, 394 skipped: 0, 395 errors: 0 396 }; 397 }); 398 stepsResults.push(blobsRes); 399 setSmartMigrationResult([...stepsResults]); 400 if (blobsRes.status === "failed") { 401 setSmartMigrationStatus("error"); 402 setIsProcessing(false); 403 return; 404 } 405 const avatarsRes = await runStep( 406 "Avatar Migration", 407 () => api.executeAvatarMigration() 408 ); 409 stepsResults.push(avatarsRes); 410 setSmartMigrationResult([...stepsResults]); 411 if (avatarsRes.status === "failed") { 412 setSmartMigrationStatus("error"); 413 setIsProcessing(false); 414 return; 415 } 416 const handlesRes = await runStep( 417 "Handle Generation", 418 () => api.generateUserHandles() 419 ); 420 stepsResults.push(handlesRes); 421 setSmartMigrationResult([...stepsResults]); 422 if (handlesRes.status === "failed") { 423 setSmartMigrationStatus("error"); 424 } else { 425 setSmartMigrationStatus("done"); 426 setLogs((prev) => [...prev, `ð Smart Migration Sequence completed successfully!`]); 427 } 428 setIsProcessing(false); 429 }; 430 const handleBlobExtraction = async () => { 431 setIsProcessing(true); 432 setLogs((prev) => [...prev, `Starting BLOB extraction from ID ${blobStartId} (Limit: ${blobLimit})...`]); 433 try { 434 const res = await api.extractMidiFiles(dbConfig, blobStartId, blobLimit); 435 setLogs((prev) => [...prev, `â ${res.message}`]); 436 if (res.lastId > blobStartId && !continueFromLatest) { 437 setBlobStartId(res.lastId + 1); 438 } 439 setSuccessCount(res.count); 440 await fetchMidiExtractionStatus(); 441 } catch (e) { 442 setLogs((prev) => [...prev, `ERROR: ${e.message}`]); 443 } finally { 444 setIsProcessing(false); 445 } 446 }; 447 const handleAvatarMigration = async () => { 448 setIsProcessing(true); 449 setLogs((prev) => [...prev, `Starting Legacy Avatar Migration...`]); 450 setSuccessCount(0); 451 try { 452 const res = await api.executeAvatarMigration(); 453 setLogs((prev) => [...prev, `â ${res.message}`]); 454 } catch (e) { 455 setLogs((prev) => [...prev, `ERROR: ${e.message}`]); 456 } finally { 457 setIsProcessing(false); 458 } 459 }; 460 const autoMapFields = (sourceKeys, targets) => { 461 const newMapping = {}; 462 let presetMap = {}; 463 if (activePreset === "users") presetMap = USER_PRESET_MAP; 464 else if (activePreset === "tracks") presetMap = TRACK_PRESET_MAP; 465 else if (activePreset === "comments") presetMap = COMMENT_PRESET_MAP;
466 targets.forEach((target) => { 467 if (presetMap[target] !== void 0) { 468 const mappedSource = presetMap[target]; 469 if (mappedSource === "") { 470 newMapping[target] = ""; 471 return; 472 } 473 if (sourceKeys.includes(mappedSource)) { 474 newMapping[target] = mappedSource; 475 return; 476 } 477 } 478 const match = sourceKeys.find( 479 (k) => k.toLowerCase() === target.toLowerCase() || k.toLowerCase().endsWith(`_${target.toLowerCase()}`) || target.toLowerCase().endsWith(`_${k.toLowerCase()}`) 480 ); 481 if (match) newMapping[target] = match; 482 if (target === "userId" && sourceKeys.includes("submitter")) newMapping[target] = "submitter"; 483 if (target === "name" && sourceKeys.includes("username")) newMapping[target] = "username"; 484 if (target === "username" && sourceKeys.includes("name")) newMapping[target] = "name"; 485 }); 486 setFieldMapping(newMapping); 487 }; 488 const updateMapping = (target, source) => { 489 setFieldMapping((prev) => ({ 490 ...prev, 491 [target]: source 492 })); 493 }; 494 const transformData = (rawData) => { 495 if (!config.targetModel) return; 496 const modelSchema = dbSchema.find((t) => t.table === config.targetModel); 497 if (!modelSchema) return; 498 const colDefMap = /* @__PURE__ */ new Map(); 499 if (modelSchema.columns) { 500 for (const col of modelSchema.columns) { 501 colDefMap.set(col.name, col); 502 } 503 } 504 const transformed = rawData.map((item) => { 505 const mappedItem = {}; 506 Object.entries(fieldMapping).forEach(([targetField, sourceField]) => { 507 if (sourceField && item[sourceField] !== void 0) { 508 let val = item[sourceField]; 509 const colDef = colDefMap.get(targetField); 510 if (colDef) { 511 const type = String(colDef.type).toLowerCase(); 512 if (type.includes("int") || type.includes("number") || type === "Int") { 513 const parsed = parseInt(val, 10); 514 val = isNaN(parsed) ? 0 : parsed; 515 } else if (type.includes("float") || type.includes("double") || type.includes("real") || type === "Float") { 516 const parsed = parseFloat(val); 517 val = isNaN(parsed) ? 0 : parsed; 518 } else if (type.includes("bool") || type === "Boolean") { 519 val = val === 1 || val === "1" || val === true || val === "true"; 520 } else if (type.includes("char") || type.includes("text") || type === "String") { 521 if (val === null || val === void 0) val = ""; 522 else val = String(val); 523 } 524 } 525 if (targetField === "createdAt" || targetField === "updatedAt" || targetField === "lastVisit" || targetField === "lastCreditReset" || targetField === "uploadedAt") { 526 val = new Date(item[sourceField]).toISOString(); 527 } 528 if (targetField === "role" && !val) val = "USER"; 529 if (targetField === "isBanned" && typeof val === "number") val = val === 1; 530 if (activePreset === "comments") { 531 if (targetField === "text") { 532 let subject = (item["Review_Subject"] || "").trim(); 533 const contents = (item["Review_Contents"] || "").trim(); 534 if (subject && !/[.!?]$/.test(subject)) { 535 subject += "."; 536 } 537 val = subject ? `${subject} 538 539${contents}` : contents; 540 } else if (targetField === "likes") { 541 const helpful = parseInt(item["Review_WasHelpful"] || "0"); 542 const notHelpful = parseInt(item["Review_WasNotHelpful"] || "0"); 543 val = helpful - notHelpful; 544 } else if (targetField === "status") { 545 const validated = (item["Review_Validated"] || "yes").toLowerCase(); 546 if (validated === "yes") val = "visible"; 547 else if (validated === "no") val = "pending"; 548 else if (validated === "deleted") val = "hidden"; 549 else val = "visible"; 550 } else if (targetField === "id" && item["ReviewID"]) { 551 val = parseInt(item["ReviewID"]); 552 } 553 } 554 if (activePreset === "tracks") { 555 if (targetField === "status") val = "completed"; 556 } 557 if (activePreset === "users") { 558 if (targetField === "role" && !val) val = "USER"; 559 if (targetField === "email" && !val) val = "[email protected]"; 560 } 561 mappedItem[targetField] = val; 562 } 563 }); 564 return { target: config.targetModel, data: mappedItem }; 565 }); 566 setPreviewData(transformed); 567 }; 568 const handleCalculateCatchUp = async () => { 569 if (!config.sourceTable || !config.targetModel) return; 570 setIsCountingCatchUp(true);
571 setLogs((prev) => [...prev, `Calculating catch-up data rows (Limit: ${fetchLimit === 0 ? "ALL" : fetchLimit}, Start ID: ${fetchStartId})...`]); 572 try { 573 const res = await api.countCatchUp(dbConfig, config.sourceTable, config.targetModel, fieldMapping, fetchLimit, fetchStartId); 574 setCatchUpData(res); 575 setLogs((prev) => [ 576 ...prev, 577 `â Catch-up calculation complete:`, 578 ` - Total source records matching query: ${res.totalSourceRows}`, 579 ` - Existing records in target DB: ${res.existingRowsCount}`, 580 ` - New records to catch-up (will be added): ${res.newRowsCount}` 581 ]); 582 } catch (e) { 583 setLogs((prev) => [...prev, `ERROR calculating catch-up: ${e.message}`]); 584 alert(`Failed to calculate catch-up: ${e.message}`); 585 } finally { 586 setIsCountingCatchUp(false); 587 } 588 }; 589 const handleExecute = async () => { 590 if (!config.sourceTable || !config.targetModel) return; 591 setIsProcessing(true); 592 const limitStr = fetchLimit === 0 ? "ALL" : fetchLimit; 593 setLogs((prev) => [ 594 ...prev, 595 `Starting server-side migration (Limit: ${limitStr})...`, 596 `â Migrating ${limitStr} records` 597 ]); 598 setSuccessCount(0); 599 try { 600 if (resetBeforeMigrating) { 601 const warningMsg = config.targetModel.toLowerCase() === "user" ? `WARNING: Enabling "Reset Table" for User will delete other accounts but preserve your active Admin session to keep you from being logged out. Do you want to proceed?` : `WARNING: Enabling "Reset Table" will completely clear all records in the '${config.targetModel}' table first. Do you want to proceed?`; 602 if (!confirm(warningMsg)) { 603 setIsProcessing(false); 604 return; 605 } 606 setLogs((prev) => [...prev, `WARNING: Initiating Reset for ${config.targetModel} before migration...`]); 607 await api.resetDatabase(config.targetModel); 608 setLogs((prev) => [...prev, `SUCCESS: ${config.targetModel} data cleared.`]); 609 setResetBeforeMigrating(false); 610 } 611 const sendVerbose = verbose || verboseErrorOnly; 612 const res = await api.executeMigration(dbConfig, config.sourceTable, config.targetModel, fieldMapping, fetchLimit, fetchStartId, overwrite, sendVerbose); 613 setSuccessCount(res.count); 614 if (sendVerbose && res.verboseLogs && res.verboseLogs.length > 0) { 615 setLogs((prev) => [...prev, ...res.verboseLogs]); 616 } 617 setLogs((prev) => [...prev, `â Migrated ${res.count} records`]); 618 } catch (e) { 619 setLogs((prev) => [...prev, `ERROR: ${e.message}`]); 620 } finally { 621 setIsProcessing(false); 622 } 623 }; 624 const handleResetDatabase = async (target = "all") => { 625 const msg = target === "all" ? "DANGER: This will delete ALL User, Track, and Comment data from the database. Are you sure?" : `WARNING: This will delete ALL records in the '${target}' table. Are you sure?`; 626 if (!confirm(msg)) return; 627 setIsProcessing(true); 628 setLogs((prev) => [...prev, `WARNING: Initiating Reset for ${target}...`]); 629 try { 630 await api.resetDatabase(target); 631 setLogs((prev) => [...prev, `SUCCESS: ${target} data cleared.`]); 632 if (target === "all") alert("Database reset complete. You can now start a fresh migration."); 633 } catch (e) { 634 setLogs((prev) => [...prev, `ERROR: Reset failed - ${e.message}`]); 635 alert("Reset failed. Check server logs."); 636 } finally { 637 setIsProcessing(false); 638 } 639 }; 640 const currentRecord = sourceData[previewIndex] || {}; 641 const currentTransformed = ((_a = previewData[previewIndex]) == null ? void 0 : _a.data) || {}; 642 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex h-full bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 overflow-hidden", children: [ 643 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `${isSidebarOpen ? "w-72" : "w-16 items-center"} bg-zinc-50 dark:bg-zinc-950 border-r border-zinc-200 dark:border-zinc-800 flex flex-col shrink-0 transition-all duration-300 shrink-0`, children: [
644 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-6 border-b border-zinc-200 dark:border-zinc-800 flex items-center ${isSidebarOpen ? "justify-between w-full" : "justify-center p-4 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-900"}`, onClick: () => !isSidebarOpen && setIsSidebarOpen(true), children: isSidebarOpen ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 645 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 646 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 647 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeftRight, { className: "text-indigo-500" }), 648 " Migration" 649 ] }), 650 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Legacy System Import Wizard" }) 651 ] }), 652 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsSidebarOpen(false), className: "text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 20 }) }) 653 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { title: "Open Migration Menu", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeftRight, { className: "text-indigo-500 cursor-pointer", size: 24 }) }) }), 654 isSidebarOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto w-full py-4", children: PRESETS.map((preset, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 655 "button", 656 { 657 onClick: () => {
658 setActivePreset(preset.id); 659 setResetBeforeMigrating(false); 660 }, 661 className: `w-full text-left px-6 py-4 border-l-4 transition-all ${activePreset === preset.id ? "bg-white dark:bg-zinc-900 border-indigo-500 shadow-sm" : "border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-900 text-zinc-500"}`, 662 children: [ 663 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `font-bold text-sm ${activePreset === preset.id ? "text-indigo-600 dark:text-indigo-400" : "text-zinc-700 dark:text-zinc-300"}`, children: preset.title }), 664 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500 mt-1 line-clamp-2", children: preset.description }) 665 ] 666 }, 667 preset.id 668 )) }), 669 isSidebarOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full p-4 border-t border-zinc-200 dark:border-zinc-800 bg-red-50/50 dark:bg-red-900/10", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 670 "button", 671 { 672 onClick: () => handleResetDatabase("all"), 673 disabled: isProcessing, 674 className: "w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 rounded-lg text-xs font-bold transition-colors disabled:opacity-50", 675 children: [
676 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 14 }), 677 "Factory Reset (All Data)" 678 ] 679 } 680 ) }) 681 ] }), 682 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [ 683 activePreset === "smart_migration" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col p-8 overflow-y-auto bg-zinc-50 dark:bg-black/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-4xl mx-auto w-full bg-white dark:bg-zinc-900 p-8 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800", children: [ 684 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mb-6", children: [ 685 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold text-xl", children: "â¡" }),
686 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 687 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "Smart Migration Sequence" }), 688 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Run all migration steps in correct order with a single click. Skips existing, copies files, and maps legacy data cleanly." }) 689 ] }) 690 ] }), 691 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 692 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800", children: [ 693 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3", children: "Target Configuration" }), 694 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 695 /* @__P
695URE__ */ jsxRuntimeExports.jsxs("div", { children: [ 696 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Source MySQL Host" }), 697 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { disabled: true, value: dbConfig.host, className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-2 text-sm text-zinc-600 dark:text-zinc-400 font-mono cursor-not-allowed" }) 698 ] }), 699 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 700 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Port" }), 701 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { disabled: true, value: dbConfig.port, className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-2 text-sm text-zinc-600 dark:text-zinc-400 font-mono cursor-not-allowed" }) 702 ] }),
703 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 704 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "User" }), 705 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { disabled: true, value: dbConfig.user, className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-2 text-sm text-zinc-600 dark:text-zinc-400 font-mono cursor-not-allowed" }) 706 ] }), 707 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 708 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Password" }), 709 /* @__PURE__ */ jsxRuntimeExports.jsx( 710 "input", 711 { 712 type: "password", 713 value: dbConfig.password, 714 onChange: (e) => setDbConfig({ ...dbConfig, password: e.target.value }), 715 placeholder: "(None required if run inside network)", 716 className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 text-zinc-900 dark:text-white" 717 } 718 ) 719 ] }) 720 ] }) 721 ] }),
722 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden", children: [ 723 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 px-4 py-3 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center", children: [ 724 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-700 dark:text-zinc-300 uppercase", children: "Sequence Progress" }), 725 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs px-2 py-0.5 rounded-full font-bold uppercase ${smartMigrationStatus === "running" ? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 animate-pulse" : smartMigrationStatus === "done" ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" : smartMigrationStatus === "error" ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" : "bg-zinc-200 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"}`, children: smartMigrationStatus === "running" ? "Running" : smartMigrationStatus === "done" ? "Completed" : smartMigrationStatus === "error" ? "Failed" : "Idle" }) 726 ] }),
727 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-zinc-200 dark:divide-zinc-800", children: [ 728 { name: "Users", desc: "Migrate users from hamienet_OEML.users" }, 729 { name: "Tracks", desc: "Migrate tracks from hamienet_MIDI.Links (resolves owners)" }, 730 { name: "Comments", desc: "Migrate review posts to Comments" }, 731 { name: "Tags", desc: "Migrate explicit tags from TagsLink joined with Tags" }, 732 { name: "MIDI Blobs", desc: "Extract MIDI binaries from MIDI table to local disk (no limit)" }, 733 { name: "Avatar Migration", desc: "Copy avatars from TBA directory and update user records" }, 734 { name: "Handle Generation", desc: "Generate unique user @handles" } 735 ].map((stepItem, idx) => { 736 const stepResult = smartMigrationResult.find((r) => r.step === stepItem.name); 737 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 flex items-center justify-between hover:bg-zinc-50/50 dark:hover:bg-zinc-900/50", children: [
738 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 739 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-sm text-zinc-900 dark:text-white flex items-center gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 740 idx + 1, 741 ". ", 742 stepItem.name 743 ] }) }), 744 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500", children: stepItem.desc }) 745 ] }), 746 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3", children: stepResult ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-right", children: [ 747 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs font-bold px-2 py-0.5 rounded ${stepResult.status === "completed" ? "bg-green-50 text-green-700 dark:bg-green-900/10 dark:text-green-400" : "bg-red-50 text-red-700 dark:bg-red-900/10 dark:text-red-400"}`, children: stepResult.status === "completed" ? "Success" : "Failed" }), 748 stepResult.status === "completed" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-zinc-400 mt-1", children: [ 749 "Processed: ", 750 stepResult.processed, 751 " | Inserted: ", 752 stepResult.inserted, 753 " | Skipped/Conflicts: ", 754 stepResult.skipped, 755 " | Errors: ", 756 stepResult.errors, 757 " (", 758 stepResult.durationMs, 759 "ms)" 760 ] }), 761 stepResult.details && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-indigo-500 dark:text-indigo-400 mt-0.5 italic", children: stepResult.details }) 762 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-medium", children: "Pending" }) }) 763 ] }, idx); 764 }) }) 765 ] }), 766 smartMigrationStatus === "done" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 bg-green-50/50 dark:bg-green-950/10 border border-green-200 dark:border-green-800/40 rounded-xl space-y-4", children: [
767 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-black text-green-900 dark:text-green-400 text-md flex items-center gap-2", children: [ 768 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 20 }), 769 "Smart Migration Summary" 770 ] }), 771 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-xs", children: [ 772 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-green-200 dark:border-green-800 text-green-800 dark:text-green-300 font-bold", children: [ 773 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Step" }), 774 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Processed" }), 775 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Inserted (New)" }),
776 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Skipped / Conflicts" }), 777 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Errors" }), 778 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "pb-2", children: "Duration" }) 779 ] }) }), 780 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-green-100 dark:divide-green-900/40 text-zinc-700 dark:text-zinc-300", children: smartMigrationResult.map((r, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "py-2", children: [ 781 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-2 font-semibold", children: r.step }), 782 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-2 font-mono", children: r.processed }), 783 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-2 font-mono text-emerald-600 dark:text-emerald-400 font-bold", children: [ 784 "+", 785 r.inserted 786 ] }),
787 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-2 font-mono", children: r.skipped }), 788 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-2 font-mono text-red-500", children: r.errors }), 789 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "py-2 font-mono", children: [ 790 r.durationMs, 791 "ms" 792 ] }) 793 ] }, i)) }) 794 ] }) }) 795 ] }), 796 /* @__PURE__ */ jsxRuntimeExports.jsxs( 797 "button", 798 { 799 onClick: handleSmartMigration, 800 disabled: isProcessing, 801 className: "w-full py-4 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white font-black text-md rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50", 802 children: [ 803 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 22 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¡" }), 804 isProcessing ? "Executing Smart Migration Sequence..." : "â¡ Run Smart Migration" 805 ] 806 } 807 ) 808 ] }) 809 ] }) }) : activePreset === "midi_blobs" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col p-8 overflow-y-auto bg-zinc-50 dark:bg-black/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl mx-auto w-full bg-white dark:bg-zinc-900 p-8 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800", children: [
810 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mb-6", children: [ 811 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl flex items-center justify-center text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { size: 24 }) }), 812 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 813 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "MIDI Blob Extractor" }), 814 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Extract binary data from legacy SQL table directly to disk." }) 815 ] }) 816 ] }), 817 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 818 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800", children: [
819 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3", children: "Target Configuration" }), 820 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 821 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 822 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Source Table" }), 823 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { disabled: true, value: "MIDI", className: "w-full bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm text-zinc-500 font-mono cursor-not-allowed" }) 824 ] }), 825 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 826 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Target Path" }),
827 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { disabled: true, value: "/public/storage/tracks/midi/{group}/{id}.mid", className: "w-full bg-zinc-200 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm text-zinc-500 font-mono cursor-not-allowed" }) 828 ] }) 829 ] }) 830 ] }), 831 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 832 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800 text-center flex flex-col justify-center min-h-[96px]", children: [ 833 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 uppercase block mb-1", children: "Latest Extracted ID" }), 834 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl font-black text-zinc-900 dark:text-white font-mono", children: maxExtractedId }) 835 ] }),
836 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800 text-center flex flex-col justify-center min-h-[96px]", children: [ 837 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 uppercase block mb-1", children: "Next ID to Extract" }), 838 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl font-black text-indigo-600 dark:text-indigo-400 font-mono", children: maxExtractedId + 1 }), 839 remainingCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-500 mt-1 font-medium", children: [ 840 "(", 841 remainingCount, 842 " remaining in DB)" 843 ] }) 844 ] }) 845 ] }), 846 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800 flex items-center justify-between", children: [
847 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col pr-4", children: [ 848 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-900 dark:text-white", children: "Auto-continue from latest" }), 849 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-500 mt-0.5", children: [ 850 "Start extraction automatically from the latest unextracted MIDI ID on disk (", 851 maxExtractedId + 1, 852 ")" 853 ] }) 854 ] }), 855 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "relative inline-flex items-center cursor-pointer shrink-0", children: [ 856 /* @__PURE__ */ jsxRuntimeExports.jsx( 857 "input", 858 { 859 type: "checkbox", 860 checked: continueFromLatest, 861 onChange: (e) => setContinueFromLatest(e.target.checked), 862 className: "sr-only peer" 863 } 864 ),
865 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-11 h-6 bg-zinc-200 peer-focus:outline-none dark:bg-zinc-700 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-zinc-600 peer-checked:bg-indigo-600" }) 866 ] }) 867 ] }), 868 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 869 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 870 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Start ID" }), 871 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 872 /* @__PURE__ */ jsxRuntimeExports.jsx( 873 "input", 874 { 875 type: "number", 876 value: blobStartId, 877 disabled: continueFromLatest, 878 onChange: (e) => setBlobStartId(parseInt(e.target.value) || 0), 879 className: `w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 text-zinc-900 dark:text-white ${continueFromLatest ? "opacity-60 cursor-not-allowed bg-zinc-50 dark:bg-zinc-900" : ""}` 880 } 881 ), 882 continueFromLatest && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute right-2 top-2 px-1.5 py-0.5 bg-indigo-50 dark:bg-indigo-900/40 border border-indigo-200 dark:border-indigo-800 text-[10px] text-indigo-600 dark:text-indigo-400 font-bold rounded-md", children: "Locked" }) 883 ] }),
884 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Starting row ID to process." }) 885 ] }), 886 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 887 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-bold text-zinc-700 dark:text-zinc-300 mb-1", children: "Batch Size (Limit)" }), 888 /* @__PURE__ */ jsxRuntimeExports.jsx( 889 "input", 890 { 891 type: "number", 892 value: blobLimit, 893 onChange: (e) => setBlobLimit(parseInt(e.target.value) || 0), 894 className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 text-zinc-900 dark:text-white" 895 } 896 ), 897 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Rows to fetch per execution." }) 898 ] }) 899 ] }),
900 /* @__PURE__ */ jsxRuntimeExports.jsxs( 901 "button", 902 { 903 onClick: handleBlobExtraction, 904 disabled: isProcessing, 905 className: "w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50", 906 children: [ 907 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 20 }), 908 isProcessing ? "Extracting Blobs..." : "Execute Batch Extraction" 909 ] 910 } 911 ) 912 ] }) 913 ] }) }) : activePreset === "avatar_migration" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col p-8 overflow-y-auto bg-zinc-50 dark:bg-black/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl mx-auto w-full bg-white dark:bg-zinc-900 p-8 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800", children: [
914 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mb-6", children: [ 915 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl flex items-center justify-center text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeftRight, { size: 24 }) }), 916 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 917 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "Legacy Avatar Migration" }), 918 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Migrate legacy filename-based avatars to structured ID-based storage." }) 919 ] }) 920 ] }), 921 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 922 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800", children: [
923 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3", children: "Process Overview" }), 924 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "text-sm text-zinc-600 dark:text-zinc-400 list-disc list-inside space-y-2", children: [ 925 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 926 "Scans ", 927 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "/storage/avatars/TBA/" }), 928 " for uploaded legacy user images." 929 ] }), 930 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 931 "Matches filenames to the ", 932 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "avatar" }), 933 " field in the Prisma ", 934 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "User" }), 935 " table." 936 ] }),
937 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 938 "Moves matched files to ", 939 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "/storage/avatars/<group>/<id>.jpg" }), 940 "." 941 ] }), 942 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: "Assigns a random default image to users without a custom legacy avatar." }), 943 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 944 "Updates the Database automatically mapping ", 945 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: '"custom"' }), 946 " or ", 947 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: '"default:<filename>"' }), 948 "." 949 ] }) 950 ] }), 951 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 p-3 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/50 rounded-lg flex gap-3 text-amber-700 dark:text-amber-400", children: [
952 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 18, className: "shrink-0" }), 953 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs", children: [ 954 "Ensure you have manually copied the legacy avatar flat files into the ", 955 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "TBA" }), 956 " folder on your storage server before proceeding. Please also seed the default collection first." 957 ] }) 958 ] }) 959 ] }), 960 /* @__PURE__ */ jsxRuntimeExports.jsxs( 961 "button", 962 { 963 onClick: handleAvatarMigration, 964 disabled: isProcessing, 965 className: "w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50", 966 children: [ 967 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 20 }), 968 isProcessing ? "Migrating Avatars..." : "Execute Full Avatar Migration" 969 ] 970 } 971 ) 972 ] }) 973 ] }) }) : activePreset === "generate_handles" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex flex-col p-8 overflow-y-auto bg-zinc-50 dark:bg-black/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-2xl mx-auto w-full bg-white dark:bg-zinc-900 p-8 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800", children: [
974 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mb-6", children: [ 975 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl flex items-center justify-center text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 24 }) }), 976 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 977 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "Unique Handle Generator" }), 978 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Assign unique @handles to all users currently missing one." }) 979 ] }) 980 ] }), 981 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 982 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800", children: [
983 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3", children: "Handle Rules" }), 984 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "text-sm text-zinc-600 dark:text-zinc-400 list-disc list-inside space-y-2", children: [ 985 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: "Characters: Alphanumeric, underscores, hyphens, and periods." }), 986 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: "No spaces allowed. All letters converted to lowercase." }), 987 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: "Length: 3â30 characters." }), 988 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 989 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Uniqueness:" }), 990 " If a collision occurs, a numeric suffix will be appended (e.g. @john, @john1)." 991 ] }), 992 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { children: [ 993 "Target: Only affects users where ",
994 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "handle" }), 995 " is currently ", 996 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-zinc-200 dark:bg-zinc-800 px-1 rounded", children: "null" }), 997 "." 998 ] }) 999 ] }) 1000 ] }), 1001 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1002 "button", 1003 { 1004 onClick: async () => { 1005 setIsProcessing(true); 1006 setLogs((prev) => [...prev, `Starting Unique Handle Generation...`]); 1007 try { 1008 const res = await api.generateUserHandles(); 1009 setLogs((prev) => [...prev, `â Successfully processed ${res.processed} users (${res.conflicts} conflicts resolved).`]); 1010 alert(`Migration Complete! Processed ${res.processed} users.`); 1011 } catch (e) { 1012 setLogs((prev) => [...prev, `ERROR: ${e.message}`]); 1013 } finally { 1014 setIsProcessing(false); 1015 } 1016 }, 1017 disabled: isProcessing, 1018 className: "w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50", 1019 children: [
1020 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 20 }), 1021 isProcessing ? "Generating..." : "Start Handle Generation" 1022 ] 1023 } 1024 ) 1025 ] }) 1026 ] }) }) : activePreset === "archive_importer" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ArchiveImporter, {}) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 1027 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 transition-all overflow-hidden ${isHeaderOpen ? "p-6" : "p-2"}`, children: isHeaderOpen ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "animate-in fade-in slide-in-from-top-2", children: [ 1028 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between mb-4", children: [ 1029 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-zinc-900 dark:text-white", children: "Connection & Source" }),
1030 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsHeaderOpen(false), className: "text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 18 }) }) 1031 ] }), 1032 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-end gap-4 mb-4", children: [ 1033 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ 1034 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Source Table (MySQL)" }), 1035 /* @__PURE__ */ jsxRuntimeExports.jsx( 1036 "input", 1037 { 1038 value: config.sourceTable, 1039 onChange: (e) => setConfig({ ...config, sourceTable: e.target.value }), 1040 placeholder: "e.g. nuke_users", 1041 className: "w-full bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm font-mono text-zinc-900 dark:text-white focus:border-indigo-500 outline-none" 1042 } 1043 ) 1044 ] }),
1045 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center text-zinc-400 pb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowRight, { size: 24 }) }), 1046 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [ 1047 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-1", children: "Target Model (Prisma)" }), 1048 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1049 "select", 1050 { 1051 value: config.targetModel, 1052 onChange: (e) => setConfig({ ...config, targetModel: e.target.value }), 1053 className: "w-full bg-zinc-50 dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-2 text-sm font-mono text-zinc-900 dark:text-white focus:border-indigo-500 outline-none", 1054 children: [ 1055 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "-- Select Model --" }), 1056 availableModels.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, children: m }, m)) 1057 ] 1058 } 1059 ) 1060 ] }), 1061 config.targetModel && /* @__PURE__ */ jsxRuntimeExports.jsx( 1062 "button", 1063 { 1064 onClick: () => handleResetDatabase(config.targetModel), 1065 disabled: isProcessing, 1066 className: "px-3 py-2 bg-red-50 dark:bg-red-900/10 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 rounded-lg text-xs font-bold transition-colors mb-[1px]", 1067 title: `Clear ${config.targetModel} Table`, 1068 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 16 }) 1069 } 1070 ) 1071 ] }), 1072 sourceMode === "direct" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-800/50 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 mb-4 grid grid-cols-12 gap-3", children: [
1073 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-4", children: [ 1074 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase mb-1", children: "Host" }), 1075 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { value: dbConfig.host, onChange: (e) => setDbConfig({ ...dbConfig, host: e.target.value }), className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-900 dark:text-white" }) 1076 ] }), 1077 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1078 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase mb-1", children: "Port" }), 1079 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "number", value: dbConfig.port, onChange: (e) => setDbConfig({ ...dbConfig, port: parseInt(e.target.value) }), className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-900 dark:text-white" }) 1080 ] }),
1081 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1082 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase mb-1", children: "User" }), 1083 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { value: dbConfig.user, onChange: (e) => setDbConfig({ ...dbConfig, user: e.target.value }), className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-900 dark:text-white" }) 1084 ] }), 1085 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1086 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase mb-1", children: "Password" }), 1087 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "password", value: dbConfig.password, onChange: (e) => setDbConfig({ ...dbConfig, password: e.target.value }), className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-900 dark:text-white" }) 1088 ] }),
1089 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1090 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase mb-1", children: "Database" }), 1091 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { value: dbConfig.database, onChange: (e) => setDbConfig({ ...dbConfig, database: e.target.value }), className: "w-full bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-900 dark:text-white" }) 1092 ] }) 1093 ] }), 1094 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-4", children: [ 1095 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-6", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 1096 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setSourceMode("direct"), className: `px-3 py-1.5 rounded text-xs font-bold transition-colors ${sourceMode === "direct" ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, children: "Direct DB Connection" }),
1097 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setSourceMode("file"), className: `px-3 py-1.5 rounded text-xs font-bold transition-colors ${sourceMode === "file" ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400" : "text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, children: "Upload Dump (JSON)" }) 1098 ] }) }), 1099 sourceMode === "direct" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 1100 "button", 1101 { 1102 onClick: handleDirectFetch, 1103 disabled: isProcessing || !config.sourceTable, 1104 className: "flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-lg text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50", 1105 children: [ 1106 /* @__PURE__ */ jsxRuntimeExports.jsx(Server, { size: 14 }), 1107 " Fetch Preview (First 50)" 1108 ] 1109 } 1110 ) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 px-4 py-2 bg-zinc-900 dark:bg-white text-white dark:text-black rounded-lg text-sm font-bold hover:opacity-90 cursor-pointer", children: [
1111 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14 }), 1112 " Upload JSON", 1113 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "file", className: "hidden", accept: ".json", onChange: handleFileUpload }) 1114 ] }) 1115 ] }) 1116 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-xs text-zinc-500", children: [ 1117 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1118 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 14 }), 1119 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 1120 dbConfig.host, 1121 " / ", 1122 dbConfig.database 1123 ] }), 1124 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowRight, { size: 12 }), 1125 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-700 dark:text-zinc-300", children: config.sourceTable || "No Source" }) 1126 ] }),
1127 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => setIsHeaderOpen(true), className: "flex items-center gap-1 hover:text-zinc-900 dark:hover:text-white", children: [ 1128 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 12 }), 1129 " Configure" 1130 ] }) 1131 ] }) }), 1132 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden flex flex-col bg-zinc-50 dark:bg-black/20", children: sourceData.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 1133 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-hidden flex flex-col md:flex-row", children: [ 1134 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-6 border-r border-zinc-200 dark:border-zinc-800", children: [ 1135 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-4", children: [ 1136 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [
1137 /* @__PURE__ */ jsxRuntimeExports.jsx(Map$1, { size: 16, className: "text-indigo-500" }), 1138 " Field Mapping" 1139 ] }), 1140 activePreset === "tracks" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col mx-4 p-4 bg-indigo-50 dark:bg-indigo-900/10 border border-indigo-200 dark:border-indigo-800/50 rounded-xl text-xs text-indigo-700 dark:text-indigo-400", children: [ 1141 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ 1142 /* @__PURE__ */ jsxRuntimeExports.jsx(Server, { size: 18, className: "shrink-0 text-indigo-500" }), 1143 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-sm", children: "Smart Hierarchy Mapping Active" }) 1144 ] }), 1145 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mb-3 leading-relaxed", children: [ 1146 "The migration engine is now synchronized with the legacy ",
1147 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Gossamer Threads" }), 1148 " hierarchy. It will automatically process categories to ensure your data is searchable and structured." 1149 ] }), 1150 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3", children: [ 1151 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-black/40 p-3 rounded-lg border border-indigo-100 dark:border-indigo-900/30", children: [ 1152 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 uppercase font-bold mb-1", children: "Title Concatenation" }), 1153 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-indigo-600 dark:text-indigo-300", children: "[Artist] - [Category] - [Title]" }), 1154 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-400 mt-1 italic", children: "Deduplicated if Artist matches Category." }) 1155 ] }),
1156 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-black/40 p-3 rounded-lg border border-indigo-100 dark:border-indigo-900/30", children: [ 1157 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 uppercase font-bold mb-1", children: "Tag Generation" }), 1158 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-1 mt-1", children: [ 1159 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded", children: "Full Hierarchy" }), 1160 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded", children: "Artist Note" }), 1161 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded line-through", children: "Structural A-Z" }) 1162 ] }) 1163 ] }) 1164 ] }),
1165 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-zinc-500 dark:text-zinc-500", children: [ 1166 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 14, className: "shrink-0" }), 1167 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px]", children: [ 1168 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Top-most categories" }), 1169 " (e.g. Broadway Musical) are automatically assigned to the ", 1170 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Genre" }), 1171 " field." 1172 ] }) 1173 ] }) 1174 ] }), 1175 activePreset === "tags_legacy" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col mx-4 p-4 bg-emerald-50 dark:bg-emerald-900/10 border border-emerald-200 dark:border-emerald-800/50 rounded-xl text-xs text-emerald-700 dark:text-emerald-400", children: [
1176 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ 1177 /* @__PURE__ */ jsxRuntimeExports.jsx(Tag, { size: 18, className: "shrink-0 text-emerald-500" }), 1178 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-sm", children: "Explicit Tag Extraction Active" }) 1179 ] }), 1180 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mb-3 leading-relaxed", children: [ 1181 "This tool migrates manually assigned keywords from the legacy ", 1182 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "TagsLink" }), 1183 " table. It joins with the main tags library to preserve community labeling." 1184 ] }), 1185 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3", children: [ 1186 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-black/40 p-3 rounded-lg border border-emerald-100 dark:border-emerald-900/30", children: [
1187 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 uppercase font-bold mb-1", children: "Deduplication" }), 1188 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-emerald-600 dark:text-emerald-300", children: "Slug-based Upsert" }), 1189 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-400 mt-1 italic", children: "Uses unique URL slugs to prevent duplicate records." }) 1190 ] }), 1191 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-black/40 p-3 rounded-lg border border-emerald-100 dark:border-emerald-900/30", children: [ 1192 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 uppercase font-bold mb-1", children: "Relationship" }), 1193 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-emerald-600 dark:text-emerald-300", children: "MidiTag Link" }),
1194 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-400 mt-1 italic", children: "Automatically connects tags to tracks via many-to-many." }) 1195 ] }) 1196 ] }) 1197 ] }), 1198 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 1199 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg p-0.5 border border-zinc-200 dark:border-zinc-700", children: [ 1200 /* @__PURE__ */ jsxRuntimeExports.jsx( 1201 "button", 1202 { 1203 onClick: () => setPreviewIndex(Math.max(0, previewIndex - 1)), 1204 disabled: previewIndex === 0, 1205 className: "p-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded disabled:opacity-30 text-zinc-600 dark:text-zinc-400", 1206 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 14 }) 1207 } 1208 ),
1209 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-mono font-bold text-zinc-500 min-w-[60px] text-center", children: [ 1210 "Row ", 1211 previewIndex + 1, 1212 "/", 1213 sourceData.length 1214 ] }), 1215 /* @__PURE__ */ jsxRuntimeExports.jsx( 1216 "button", 1217 { 1218 onClick: () => setPreviewIndex(Math.min(sourceData.length - 1, previewIndex + 1)), 1219 disabled: previewIndex === sourceData.length - 1, 1220 className: "p-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded disabled:opacity-30 text-zinc-600 dark:text-zinc-400", 1221 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 1222 } 1223 ) 1224 ] }), 1225 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-zinc-500 hidden sm:block", children: [
1226 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold", children: config.sourceTable }), 1227 " â ", 1228 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold", children: config.targetModel }) 1229 ] }) 1230 ] }) 1231 ] }), 1232 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 1233 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-12 gap-2 text-xs font-bold text-zinc-500 uppercase px-3", children: [ 1234 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-3", children: "Target Field" }), 1235 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-4", children: "Source Field" }), 1236 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-5", children: "Preview Value" }) 1237 ] }), 1238 targetFields.map((target) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-12 gap-2 items-center bg-white dark:bg-zinc-900 p-2 rounded-lg border border-zinc-200 dark:border-zinc-800 shadow-sm", children: [
1239 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-3 font-mono text-xs font-bold text-zinc-700 dark:text-zinc-300 truncate", title: target, children: target }), 1240 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-4", children: activePreset === "comments" && (target === "text" || target === "likes" || target === "status") ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-500 italic", children: [ 1241 target === "text" && "Subject + Contents (Special)", 1242 target === "likes" && "Helpful - NotHelpful (Special)", 1243 target === "status" && "yesâvisible, noâpending, delâhidden" 1244 ] }) : activePreset === "tracks" && target === "status" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-500 italic", children: "Fixed: completed" }) : activePreset === "users" && target === "role" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-1.5 text-xs text-zinc-500 italic", children: "Default: USER" }) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 1245 "select", 1246 { 1247 value: fieldMapping[target] || "", 1248 onChange: (e) => updateMapping(target, e.target.value), 1249 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", 1250 children: [
1251 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "-- Ignore --" }), 1252 sourceFields.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: s, children: s }, s)) 1253 ] 1254 } 1255 ) }), 1256 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-zinc-100 dark:bg-zinc-950/50 p-1.5 rounded text-xs font-mono text-zinc-500 truncate min-h-[24px]", children: currentTransformed[target] !== void 0 ? String(currentTransformed[target]) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-30 italic", children: "null" }) }) }) 1257 ] }, target)) 1258 ] }) 1259 ] }), 1260 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full md:w-80 bg-zinc-100 dark:bg-zinc-950/50 p-4 border-t md:border-t-0 border-zinc-200 dark:border-zinc-800 flex flex-col", children: [
1261 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-4", children: [ 1262 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-zinc-900 dark:text-white text-sm", children: "Source Record Inspector" }), 1263 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs bg-zinc-200 dark:bg-zinc-800 px-2 py-0.5 rounded text-zinc-600 dark:text-zinc-400", children: [ 1264 sourceData.length, 1265 " records" 1266 ] }) 1267 ] }), 1268 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between bg-white dark:bg-zinc-900 p-2 rounded-lg border border-zinc-200 dark:border-zinc-800 mb-4 shadow-sm", children: [ 1269 /* @__PURE__ */ jsxRuntimeExports.jsx( 1270 "button", 1271 { 1272 onClick: () => setPreviewIndex(Math.max(0, previewIndex - 1)), 1273 disabled: previewIndex === 0, 1274 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded disabled:opacity-30", 1275 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 16 }) 1276 } 1277 ),
1278 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-mono font-bold", children: [ 1279 "Record ", 1280 previewIndex + 1, 1281 " / ", 1282 sourceData.length 1283 ] }), 1284 /* @__PURE__ */ jsxRuntimeExports.jsx( 1285 "button", 1286 { 1287 onClick: () => setPreviewIndex(Math.min(sourceData.length - 1, previewIndex + 1)), 1288 disabled: previewIndex === sourceData.length - 1, 1289 className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded disabled:opacity-30", 1290 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 16 }) 1291 } 1292 ) 1293 ] }), 1294 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-3 shadow-inner custom-scrollbar", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-[10px] font-mono text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap break-all", children: JSON.stringify(currentRecord, null, 2) }) }) 1295 ] }) 1296 ] }),
1297 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-white dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center shrink-0", children: [ 1298 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 1299 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1300 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase", children: "Migration Limit (0=All)" }), 1301 /* @__PURE__ */ jsxRuntimeExports.jsx( 1302 "input", 1303 { 1304 type: "number", 1305 min: 0, 1306 value: fetchLimit, 1307 onChange: (e) => setFetchLimit(parseInt(e.target.value)), 1308 className: "w-24 bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded p-1 text-sm outline-none text-zinc-900 dark:text-white" 1309 } 1310 ) 1311 ] }),
1312 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1313 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[10px] font-bold text-zinc-500 uppercase", children: "Starting ID (0=All)" }), 1314 /* @__PURE__ */ jsxRuntimeExports.jsx( 1315 "input", 1316 { 1317 type: "number", 1318 min: 0, 1319 value: fetchStartId, 1320 onChange: (e) => setFetchStartId(parseInt(e.target.value)), 1321 className: "w-24 bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-700 rounded p-1 text-sm outline-none text-zinc-900 dark:text-white" 1322 } 1323 ) 1324 ] }), 1325 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-8 w-px bg-zinc-200 dark:bg-zinc-800 mx-2" }), 1326 catchUpData && !overwrite ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs shrink-0 max-w-[250px]", children: [
1327 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-bold text-indigo-600 dark:text-indigo-400 flex items-center gap-1", children: [ 1328 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 12, className: "text-emerald-500" }), 1329 " Catch-Up Stats" 1330 ] }), 1331 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-zinc-500 whitespace-nowrap overflow-hidden text-ellipsis", children: [ 1332 "Total: ", 1333 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-700 dark:text-zinc-300", children: catchUpData.totalSourceRows }), 1334 " | New: ", 1335 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-emerald-600 dark:text-emerald-400", children: [ 1336 "+", 1337 catchUpData.newRowsCount 1338 ] }), 1339 " | Skip: ", 1340 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-500", children: catchUpData.existingRowsCount }) 1341 ] }) 1342 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-zinc-500", children: [
1343 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-indigo-600 dark:text-indigo-400", children: "Server-Side Execution" }), 1344 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Data will be streamed directly between databases." }) 1345 ] }) 1346 ] }), 1347 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 1348 !overwrite && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1349 "button", 1350 { 1351 onClick: handleCalculateCatchUp, 1352 disabled: isProcessing || isCountingCatchUp, 1353 className: "px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-950 dark:hover:bg-indigo-900 text-indigo-600 dark:text-indigo-400 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-colors disabled:opacity-50", 1354 title: "Check how many new rows will be added to catch-up before starting migration", 1355 children: [ 1356 isCountingCatchUp ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 12 }), 1357 "Check Catch-Up Rows" 1358 ] 1359 } 1360 ),
1361 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", title: "Overwrite existing records with matching IDs", children: [ 1362 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-8 h-4 rounded-full transition-colors relative ${overwrite ? "bg-indigo-500" : "bg-zinc-300 dark:bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform ${overwrite ? "translate-x-4" : ""}` }) }), 1363 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", className: "hidden", checked: overwrite, onChange: (e) => setOverwrite(e.target.checked) }), 1364 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 group-hover:text-indigo-600 transition-colors", children: "Overwrite Existing" }) 1365 ] }), 1366 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", title: "Log SQL queries and Prisma operations to the console", children: [
1367 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-8 h-4 rounded-full transition-colors relative ${verbose ? "bg-amber-500" : "bg-zinc-300 dark:bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform ${verbose ? "translate-x-4" : ""}` }) }), 1368 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", className: "hidden", checked: verbose, onChange: (e) => { 1369 setVerbose(e.target.checked); 1370 if (e.target.checked) { 1371 setVerboseErrorOnly(false); 1372 } 1373 } }), 1374 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 group-hover:text-amber-600 transition-colors", children: "Verbose Log" }) 1375 ] }), 1376 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", title: "Filter verbose logs to show only warning, skip, alignment, and error details", children: [
1377 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-8 h-4 rounded-full transition-colors relative ${verboseErrorOnly ? "bg-rose-500" : "bg-zinc-300 dark:bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform ${verboseErrorOnly ? "translate-x-4" : ""}` }) }), 1378 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", className: "hidden", checked: verboseErrorOnly, onChange: (e) => { 1379 setVerboseErrorOnly(e.target.checked); 1380 if (e.target.checked) { 1381 setVerbose(false); 1382 } 1383 } }), 1384 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 group-hover:text-rose-600 transition-colors", children: "Errors Only" }) 1385 ] }), 1386 config.targetModel && /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", title: `Wipe all records from the ${config.targetModel} table before migrating`, children: [
1387 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-8 h-4 rounded-full transition-colors relative ${resetBeforeMigrating ? "bg-red-500" : "bg-zinc-300 dark:bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform ${resetBeforeMigrating ? "translate-x-4" : ""}` }) }), 1388 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", className: "hidden", checked: resetBeforeMigrating, onChange: (e) => setResetBeforeMigrating(e.target.checked) }), 1389 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 group-hover:text-red-500 transition-colors", children: "Reset Table" }) 1390 ] }), 1391 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1392 "button", 1393 { 1394 onClick: handleExecute, 1395 disabled: isProcessing, 1396 className: "px-6 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-bold flex items-center gap-2 shadow-lg shadow-emerald-500/20 disabled:opacity-50", 1397 children: [
1398 isProcessing ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 16 }), 1399 isProcessing ? "Migrating..." : "Start Migration" 1400 ] 1401 } 1402 ) 1403 ] }) 1404 ] }) 1405 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center flex-1 text-zinc-400", children: [ 1406 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 48, className: "mb-4 opacity-20" }), 1407 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Configure source and fetch preview to begin mapping." }) 1408 ] }) }) 1409 ] }), 1410 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `border-t border-zinc-200 dark:border-zinc-800 bg-black transition-all ${isLogsOpen ? "h-48" : "h-8"} flex flex-col`, children: [ 1411 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-8 flex items-center justify-between px-4 bg-zinc-900 border-b border-zinc-800 cursor-pointer", onClick: () => setIsLogsOpen(!isLogsOpen), children: [
1412 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] font-bold text-zinc-500 uppercase flex items-center gap-2", children: [ 1413 /* @__PURE__ */ jsxRuntimeExports.jsx(Server, { size: 10 }), 1414 " Migration Console" 1415 ] }), 1416 isLogsOpen ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14, className: "text-zinc-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14, className: "text-zinc-500" }) 1417 ] }), 1418 isLogsOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4 font-mono text-xs text-zinc-400 custom-scrollbar animate-fadeIn", children: [ 1419 (() => { 1420 const displayedLogs = verboseErrorOnly ? logs.filter( 1421 (log) => log.startsWith("[ERROR]") || log.startsWith("[SKIP]") || log.startsWith("[ALIGN ID]") || log.startsWith("Starting") || log.startsWith("â") || log.startsWith("ERROR:") || log.startsWith("WARNING:") || log.startsWith("SUCCESS:") 1422 ) : logs; 1423 if (displayedLogs.length === 0) { 1424 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-30", children: "Waiting for actions..." }); 1425 } 1426 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 1427 displayedLogs.map((log, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `mb-1 ${log.startsWith("[ERROR]") || log.startsWith("ERROR:") ? "text-rose-400 font-bold" : log.startsWith("[SKIP]") ? "text-amber-400 font-semibold" : log.startsWith("[ALIGN ID]") ? "text-sky-400 font-semibold" : log.startsWith("â") ? "text-emerald-400 font-bold" : "text-zinc-400"}`, children: log }, i)), 1428 verboseErrorOnly && logs.some((l) => l.startsWith("[OK]")) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-zinc-600 italic text-[10px] mt-2 border-t border-zinc-900/50 pt-1", children: '* Standard [OK] database operations have been filtered from view. Uncheck "Errors Only" to inspect them.' }) 1429 ] }); 1430 })(), 1431 successCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-emerald-500 font-bold mt-2", children: [ 1432 "â Operation completed: ", 1433 successCount, 1434 " records processed." 1435 ] }) 1436 ] }) 1437 ] }) 1438 ] }) 1439 ] }); 1440}; 1441export { 1442 DataMigration as default 1443};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.