refactor(I5): remove one-and-done GS/IR migration tooling
The GS/IR migration cutover is complete, so the runbook tooling is dead weight. Removed: - services/migrators/ (gs_ingest, ir_ingest, tag_apply, ml_queue, verify, cleanup), tasks/migration.py, api/migrate.py (+ blueprint registration) - MigrationRun model; alembic 0027 drops the migration_run table - frontend LegacyMigrationCard + migration store (+ MaintenancePanel ref) - celery include + task route + celery_signals queue mapping for migration.* - the 1 GB MAX_CONTENT_LENGTH / MAX_FORM_MEMORY override (added solely for the ir_ingest upload) - migration-surface tests (test_api_migrate, test_migration_verify, test_ir_ingest, test_gs_ingest, test_tag_apply) Kept: the alembic schema-migration tests (test_migration_00XX — unrelated) and cleanup_service.py (the permanent artist-cascade/unlink home). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,245 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Legacy migration</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-3">
|
||||
Migrate existing ImageRepo and GallerySubscriber data into FabledCurator.
|
||||
Each app produces a JSON export file via a small script in its own repo
|
||||
(<code>scripts/export_for_fabledcurator.py</code>); upload both files
|
||||
here and run the steps in order.
|
||||
</p>
|
||||
|
||||
<ol class="text-body-2 mb-3 fc-migrate__hints">
|
||||
<li>Backup FC (creates a pre-migration snapshot).</li>
|
||||
<li>Upload + ingest GS export.</li>
|
||||
<li>Upload + ingest IR export.</li>
|
||||
<li>Switch to <strong>Import tab</strong> → trigger the existing FC filesystem scan over the bind-mounted IR images dir.</li>
|
||||
<li>Return here → apply IR tag associations (joins by sha256).</li>
|
||||
<li>Queue ML re-processing.</li>
|
||||
<li>Verify.</li>
|
||||
</ol>
|
||||
|
||||
<div class="fc-migrate__uploads">
|
||||
<v-file-input
|
||||
v-model="gsFile"
|
||||
label="GallerySubscriber export (gs-export.json)"
|
||||
accept="application/json,.json"
|
||||
density="compact"
|
||||
show-size
|
||||
prepend-icon="mdi-upload"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="irFile"
|
||||
label="ImageRepo export (ir-export.json)"
|
||||
accept="application/json,.json"
|
||||
density="compact"
|
||||
show-size
|
||||
prepend-icon="mdi-upload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fc-migrate__steps mt-3">
|
||||
<v-btn
|
||||
color="accent" variant="tonal" prepend-icon="mdi-content-save"
|
||||
:disabled="store.isRunning"
|
||||
@click="onBackup"
|
||||
>1. Backup FC</v-btn>
|
||||
<v-btn
|
||||
color="primary" variant="tonal" prepend-icon="mdi-database-arrow-right"
|
||||
:disabled="store.isRunning || !gsFile"
|
||||
@click="onIngestGs"
|
||||
>2. Ingest GS</v-btn>
|
||||
<v-btn
|
||||
color="primary" variant="tonal" prepend-icon="mdi-database-arrow-right"
|
||||
:disabled="store.isRunning || !irFile"
|
||||
@click="onIngestIr"
|
||||
>3. Ingest IR</v-btn>
|
||||
<v-btn
|
||||
color="primary" variant="tonal" prepend-icon="mdi-tag-multiple"
|
||||
:disabled="store.isRunning"
|
||||
@click="onTagApply"
|
||||
>5. Apply IR tags</v-btn>
|
||||
<v-btn
|
||||
variant="outlined" prepend-icon="mdi-brain"
|
||||
:disabled="store.isRunning"
|
||||
@click="onMlQueue"
|
||||
>6. Queue ML</v-btn>
|
||||
<v-btn
|
||||
variant="outlined" prepend-icon="mdi-check-circle"
|
||||
:disabled="store.isRunning"
|
||||
@click="onVerify"
|
||||
>7. Verify</v-btn>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="error" variant="outlined" prepend-icon="mdi-restore" class="mt-3"
|
||||
:disabled="store.isRunning"
|
||||
@click="onRollback"
|
||||
>Rollback to pre-migration backup</v-btn>
|
||||
|
||||
<v-alert v-if="store.error" type="error" variant="tonal" class="mt-3" closable>
|
||||
{{ String(store.error) }}
|
||||
</v-alert>
|
||||
|
||||
<v-card v-if="store.activeRun" variant="outlined" class="mt-4">
|
||||
<v-card-text>
|
||||
<div class="text-subtitle-2">
|
||||
#{{ store.activeRun.id }} — {{ store.activeRun.kind }}
|
||||
({{ store.activeRun.status }})
|
||||
</div>
|
||||
<v-progress-linear
|
||||
v-if="store.isRunning" indeterminate color="accent" class="my-2"
|
||||
/>
|
||||
<div class="text-caption">
|
||||
Rows: {{ store.activeRun.counts.rows_processed || 0 }} processed,
|
||||
{{ store.activeRun.counts.rows_inserted || 0 }} inserted,
|
||||
{{ store.activeRun.counts.rows_skipped || 0 }} skipped.
|
||||
Conflicts: {{ store.activeRun.counts.conflicts || 0 }}.
|
||||
</div>
|
||||
<div v-if="store.activeRun.error" class="text-error mt-1">
|
||||
{{ store.activeRun.error }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div v-if="store.recentRuns.length" class="mt-4">
|
||||
<div class="text-subtitle-2 mb-2">Recent runs</div>
|
||||
<ul class="fc-migrate__history">
|
||||
<li v-for="r in store.recentRuns" :key="r.id">
|
||||
#{{ r.id }} {{ r.kind }}
|
||||
<v-chip
|
||||
size="x-small"
|
||||
:color="r.status === 'ok' ? 'success' : (r.status === 'error' ? 'error' : undefined)"
|
||||
>{{ r.status }}</v-chip>
|
||||
<span class="text-caption fc-migrate__when">{{ r.started_at }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog v-model="confirmOpen" max-width="520">
|
||||
<v-card>
|
||||
<v-card-title>Confirm {{ pendingLabel }}</v-card-title>
|
||||
<v-card-text>
|
||||
This will modify FC's database. Make sure you've taken a backup first.
|
||||
Continue?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="confirmOpen = false">Cancel</v-btn>
|
||||
<v-btn color="primary" variant="tonal" @click="onConfirm">Continue</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useMigrationStore } from '../../stores/migration.js'
|
||||
|
||||
const store = useMigrationStore()
|
||||
|
||||
const gsFile = ref(null)
|
||||
const irFile = ref(null)
|
||||
|
||||
const confirmOpen = ref(false)
|
||||
const pendingLabel = ref('')
|
||||
const pendingAction = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadRecent()
|
||||
})
|
||||
|
||||
function _pick(model) {
|
||||
if (!model) return null
|
||||
// v-file-input v-model can be a single File or a [File] depending on Vuetify version.
|
||||
return Array.isArray(model) ? model[0] : model
|
||||
}
|
||||
|
||||
function requestConfirm(label, action) {
|
||||
pendingLabel.value = label
|
||||
pendingAction.value = action
|
||||
confirmOpen.value = true
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
confirmOpen.value = false
|
||||
const action = pendingAction.value
|
||||
pendingAction.value = null
|
||||
if (action) await action()
|
||||
}
|
||||
|
||||
async function onBackup() {
|
||||
await store.trigger('backup', { tag: 'pre_migration' })
|
||||
}
|
||||
|
||||
async function onIngestGs() {
|
||||
const file = _pick(gsFile.value)
|
||||
if (!file) return
|
||||
requestConfirm('Ingest GS', async () => {
|
||||
await store.trigger('gs_ingest', { dry_run: false }, file)
|
||||
})
|
||||
}
|
||||
|
||||
async function onIngestIr() {
|
||||
const file = _pick(irFile.value)
|
||||
if (!file) return
|
||||
requestConfirm('Ingest IR', async () => {
|
||||
await store.trigger('ir_ingest', { dry_run: false }, file)
|
||||
})
|
||||
}
|
||||
|
||||
async function onTagApply() {
|
||||
requestConfirm('Apply IR tag associations', async () => {
|
||||
await store.trigger('tag_apply', { dry_run: false })
|
||||
})
|
||||
}
|
||||
|
||||
async function onMlQueue() {
|
||||
await store.trigger('ml_queue', {})
|
||||
}
|
||||
|
||||
async function onVerify() {
|
||||
await store.trigger('verify', {})
|
||||
}
|
||||
|
||||
async function onRollback() {
|
||||
requestConfirm('Rollback to pre-migration backup', async () => {
|
||||
await store.trigger('rollback', {})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-migrate__hints {
|
||||
padding-left: 1.2rem;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
.fc-migrate__uploads {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.fc-migrate__steps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.fc-migrate__history {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.fc-migrate__history li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgb(var(--v-theme-on-surface-variant), 0.15);
|
||||
}
|
||||
.fc-migrate__when {
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -17,8 +17,8 @@
|
||||
<BackupCard class="mt-6" />
|
||||
<!-- TagMaintenanceCard moved to Cleanup tab (v26.05.25.7) — it
|
||||
operates on the existing library which fits the Cleanup-tab
|
||||
theme, and clusters with the other audit cards. -->
|
||||
<LegacyMigrationCard class="mt-6" />
|
||||
theme, and clusters with the other audit cards. LegacyMigrationCard
|
||||
removed once the one-and-done GS/IR migration cutover completed. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,7 +32,6 @@ import MLThresholdSliders from './MLThresholdSliders.vue'
|
||||
import AllowlistTable from './AllowlistTable.vue'
|
||||
import AliasTable from './AliasTable.vue'
|
||||
import BackupCard from './BackupCard.vue'
|
||||
import LegacyMigrationCard from './LegacyMigrationCard.vue'
|
||||
import { useSystemActivityStore } from '../../stores/systemActivity.js'
|
||||
|
||||
// Poll queue depths so each card's QueueStatusBar shows live pending
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useApi } from '../composables/useApi.js'
|
||||
|
||||
const POLL_MS = 2000
|
||||
|
||||
export const useMigrationStore = defineStore('migration', () => {
|
||||
const api = useApi()
|
||||
|
||||
const activeRun = ref(null)
|
||||
const recentRuns = ref([])
|
||||
const error = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
let pollHandle = null
|
||||
|
||||
async function trigger(kind, params, file = null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
let body
|
||||
if (file) {
|
||||
// Multipart upload for ingest kinds.
|
||||
const form = new FormData()
|
||||
form.append('export_file', file)
|
||||
if (params && params.dry_run) form.append('dry_run', 'true')
|
||||
if (params && params.force) form.append('force', 'true')
|
||||
body = await api.post(`/api/migrate/${kind}`, { body: form })
|
||||
} else {
|
||||
body = await api.post(`/api/migrate/${kind}`, { body: params || {} })
|
||||
}
|
||||
await loadRun(body.run_id)
|
||||
pollActive(body.run_id)
|
||||
return body
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRun(runId) {
|
||||
activeRun.value = await api.get(`/api/migrate/runs/${runId}`)
|
||||
}
|
||||
|
||||
async function loadRecent() {
|
||||
recentRuns.value = await api.get('/api/migrate/runs', { params: { limit: 10 } })
|
||||
}
|
||||
|
||||
function pollActive(runId) {
|
||||
stopPolling()
|
||||
pollHandle = setInterval(async () => {
|
||||
try {
|
||||
const run = await api.get(`/api/migrate/runs/${runId}`)
|
||||
activeRun.value = run
|
||||
if (run.status === 'ok' || run.status === 'error') {
|
||||
stopPolling()
|
||||
await loadRecent()
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
stopPolling()
|
||||
}
|
||||
}, POLL_MS)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollHandle) {
|
||||
clearInterval(pollHandle)
|
||||
pollHandle = null
|
||||
}
|
||||
}
|
||||
|
||||
const isRunning = computed(
|
||||
() => activeRun.value && activeRun.value.status === 'running',
|
||||
)
|
||||
|
||||
return {
|
||||
activeRun, recentRuns, error, loading, isRunning,
|
||||
trigger, loadRun, loadRecent, pollActive, stopPolling,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user