fc5: LegacyMigrationCard — file uploads + step buttons + active-run + history; slotted into MaintenancePanel

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:04:24 -04:00
parent b600cf6e86
commit a9b354780b
2 changed files with 247 additions and 0 deletions
@@ -0,0 +1,245 @@
<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>
@@ -12,6 +12,7 @@
<MLThresholdSliders class="mt-4" />
<AllowlistTable class="mt-4" />
<AliasTable class="mt-4" />
<LegacyMigrationCard class="mt-6" />
</div>
</template>
@@ -21,6 +22,7 @@ import CentroidRecomputeCard from './CentroidRecomputeCard.vue'
import MLThresholdSliders from './MLThresholdSliders.vue'
import AllowlistTable from './AllowlistTable.vue'
import AliasTable from './AliasTable.vue'
import LegacyMigrationCard from './LegacyMigrationCard.vue'
</script>
<style scoped>